🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@cometchat/skills-native

Package Overview
Dependencies
Maintainers
12
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cometchat/skills-native - npm Package Compare versions

Comparing version
1.0.1
to
2.0.0
+41
-194
bin/install.js

@@ -5,209 +5,56 @@ #!/usr/bin/env node

const fs = require("fs");
const path = require("path");
const os = require("os");
// @cometchat/skills-native is deprecated as of @cometchat/skills@4.0.0.
//
// The unified package auto-detects React Native (Expo / bare RN) and installs
// the same skills. This wrapper preserves backward compatibility for users who
// still type the old command — it prints a deprecation banner and forwards the
// invocation to `@cometchat/skills add ... --family native`.
//
// We force --family native so the wrapper always behaves the way the legacy
// package did, even if the user's cwd somehow detects as web. Any --family flag
// the user passes is filtered out — this package's contract is "RN skills."
// ── Lazy-load optional deps ───────────────────────────────────────────────────
const { spawnSync } = require("child_process");
function tryRequire(name) {
try { return require(name); } catch { return null; }
}
const chalk = tryRequire("chalk");
const ora = tryRequire("ora");
const yellow = (s) => chalk ? chalk.yellow(s) : s;
const cyan = (s) => chalk ? chalk.cyan(s) : s;
const c = {
bold: (s) => chalk ? chalk.bold(s) : s,
cyan: (s) => chalk ? chalk.cyan(s) : s,
green: (s) => chalk ? chalk.green(s) : s,
yellow: (s) => chalk ? chalk.yellow(s) : s,
gray: (s) => chalk ? chalk.gray(s) : s,
red: (s) => chalk ? chalk.red(s) : s,
dim: (s) => chalk ? chalk.dim(s) : s,
};
console.log(yellow(`
──────────────────────────────────────────────────────────────────
@cometchat/skills-native is deprecated as of @cometchat/skills@4.0.0.
// ── Skills registry ───────────────────────────────────────────────────────────
// SKILLS_SRC_DIR resolves to the skills/ folder next to this bin/ directory
// when published. In the monorepo, it points back to the top-level skills/.
const SKILLS_SRC_DIR = (() => {
// Published layout: packages/skills-native/skills/ is populated at publish time
const local = path.join(__dirname, "..", "skills");
if (fs.existsSync(path.join(local, "cometchat-native-core", "SKILL.md"))) return local;
// Monorepo dev layout: pull from top-level skills/
return path.join(__dirname, "..", "..", "..", "skills");
})();
The unified package auto-detects React Native and installs the
same skills:
const SKILLS = [
// Dispatcher (shared across web + RN packages — content is identical;
// last-installed wins on collision and behavior is the same either way)
{ name: "cometchat", description: "Entry point — detects framework (web or React Native), handles onboarding, guides integration" },
// Core (always installed)
{ name: "cometchat-native-core", description: "Foundational rules — init, login, provider chain, env vars, auth tokens, anti-patterns" },
{ name: "cometchat-native-components", description: "Component catalog — names, props, slot views, request builders" },
{ name: "cometchat-native-placement", description: "Where to put chat — stack screen, bottom tab, modal, bottom sheet, embedded" },
// Framework patterns
{ name: "cometchat-native-expo-patterns", description: "Expo managed workflow patterns + Expo Router + app.json config" },
{ name: "cometchat-native-bare-patterns", description: "Bare React Native patterns + pod install + native modules + privacy manifest" },
// Phase B
{ name: "cometchat-native-theming", description: "CometChatThemeProvider — colors, typography, dark mode, per-component styles" },
{ name: "cometchat-native-features", description: "Feature catalog — calls, extensions, AI agent, reactions, polls" },
{ name: "cometchat-native-customization", description: "Custom text formatters, events, request builder filtering, DataSource decorators" },
{ name: "cometchat-native-production", description: "Server-minted auth tokens, user management CRUD, external-backend recipes" },
{ name: "cometchat-native-push", description: "Push notifications — APNs + FCM setup, dashboard providers, client registration, tap-to-deep-link" },
{ name: "cometchat-native-testing", description: "Testing — Jest + RNTL mocking the UI Kit/SDK, component tests, E2E with Detox vs Maestro, CI" },
{ name: "cometchat-native-troubleshooting", description: "Metro cache, pod install, iOS privacy, native module linking" },
];
${cyan("npx @cometchat/skills add")}
// ── Helpers ───────────────────────────────────────────────────────────────────
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
This wrapper continues to work — it forwards your command to the
unified installer with --family native. Update your scripts and
docs at your convenience.
──────────────────────────────────────────────────────────────────
`));
// ── IDE target directories ────────────────────────────────────────────────────
const IDE_TARGETS = {
claude: { local: ".claude/skills", global: path.join(os.homedir(), ".claude", "skills"), skillFile: "SKILL.md" },
cursor: { local: ".cursor/skills", global: path.join(os.homedir(), ".cursor", "skills"), skillFile: "SKILL.md" },
kiro: { local: ".kiro/skills", global: path.join(os.homedir(), ".kiro", "skills"), skillFile: "SKILL.md" },
copilot: { local: ".github", global: null, skillFile: null },
};
function installSkill(skill, baseDir, skillFile) {
const src = path.join(SKILLS_SRC_DIR, skill.name, "SKILL.md");
if (!fs.existsSync(src)) {
throw new Error(`SKILL.md not found at ${src} — package may have been published without skills/`);
// Forward all user args to the unified installer, but force --family native.
// Strip any user-passed --family flag (deprecated package's contract is "RN").
const userArgs = process.argv.slice(2);
const filtered = [];
for (let i = 0; i < userArgs.length; i++) {
if (userArgs[i] === "--family") {
i++; // skip the value too
continue;
}
const skillDir = path.join(baseDir, skill.name);
ensureDir(skillDir);
const dest = path.join(skillDir, skillFile);
fs.copyFileSync(src, dest);
const refsDir = path.join(SKILLS_SRC_DIR, skill.name, "references");
if (fs.existsSync(refsDir)) {
const destRefs = path.join(skillDir, "references");
ensureDir(destRefs);
for (const file of fs.readdirSync(refsDir)) {
fs.copyFileSync(path.join(refsDir, file), path.join(destRefs, file));
}
}
return dest;
filtered.push(userArgs[i]);
}
const finalArgs = [...filtered, "--family", "native"];
function installCopilotSkills(baseDir) {
ensureDir(baseDir);
const dest = path.join(baseDir, "copilot-instructions.md");
let content = "# CometChat React Native Skills\n\n";
for (const skill of SKILLS) {
const src = path.join(SKILLS_SRC_DIR, skill.name, "SKILL.md");
if (!fs.existsSync(src)) continue;
content += fs.readFileSync(src, "utf8") + "\n\n---\n\n";
}
fs.writeFileSync(dest, content, "utf8");
return dest;
}
const result = spawnSync(
"npx",
["-y", "@cometchat/skills@latest", ...finalArgs],
{ stdio: "inherit", shell: false }
);
function printHelp() {
console.log(`
${c.bold("cometchat-skills-native")} — Install CometChat AI coding skills for React Native
${c.bold("Usage:")}
${c.cyan("npx @cometchat/skills-native add")} Install for Claude Code (default)
${c.cyan("npx @cometchat/skills-native add --ide cursor")} Install for Cursor
${c.cyan("npx @cometchat/skills-native add --ide kiro")} Install for Kiro
${c.cyan("npx @cometchat/skills-native add --ide copilot")} Install for VS Code Copilot
${c.cyan("npx @cometchat/skills-native add --ide all")} Install for all supported IDEs
${c.cyan("npx @cometchat/skills-native add --global")} Install globally
${c.cyan("npx @cometchat/skills-native add --list")} Show available skills
${c.bold("Supported IDEs:")} claude, cursor, kiro, copilot, all
${c.bold("After installing, open your React Native / Expo project in your IDE and run:")}
${c.cyan("/cometchat")}
`);
}
async function main() {
const args = process.argv.slice(2);
const cmd = args[0];
if (!cmd || cmd === "--help" || cmd === "-h") {
printHelp();
process.exit(0);
}
if (cmd !== "add") {
console.error(c.red(`\n Unknown command: ${cmd}`));
printHelp();
process.exit(1);
}
const isGlobal = args.includes("--global") || args.includes("-g");
const listOnly = args.includes("--list") || args.includes("-l");
const ideIdx = args.indexOf("--ide");
const ideArg = ideIdx !== -1 ? args[ideIdx + 1] : "claude";
if (listOnly) {
console.log(`\n ${c.bold("Available skills:")}\n`);
for (const s of SKILLS) {
console.log(` ${c.cyan(s.name)}`);
console.log(` ${c.dim(s.description)}\n`);
}
process.exit(0);
}
const targets = ideArg === "all" ? Object.keys(IDE_TARGETS) : [ideArg];
for (const ide of targets) {
const target = IDE_TARGETS[ide];
if (!target) {
console.error(c.red(`\n Unknown IDE: ${ide}. Valid: ${Object.keys(IDE_TARGETS).join(", ")}, all`));
process.exit(1);
}
if (isGlobal && !target.global) {
console.log(`\n ${c.yellow("⚠")} ${ide} does not support global install — skipping.`);
continue;
}
const baseDir = isGlobal ? target.global : path.join(process.cwd(), target.local);
const scopeLabel = isGlobal
? `global ${c.gray(`(${target.global}/`)}`
: `local ${c.gray(`(${target.local}/)`)}`;
console.log(`\n ${c.bold(c.cyan("CometChat React Native Skills"))} — ${ide} integration\n`);
console.log(` Scope: ${scopeLabel}\n`);
ensureDir(baseDir);
if (ide === "copilot") {
const spinner = ora ? ora({ text: "copilot-instructions.md", prefixText: " " }).start() : null;
try {
installCopilotSkills(baseDir);
if (spinner) spinner.succeed(c.green("✓ ") + c.bold("copilot-instructions.md") + ` ${c.dim("All native skills concatenated")}`);
else console.log(` ✓ copilot-instructions.md`);
} catch (err) {
if (spinner) spinner.fail(c.red(`✗ copilot-instructions.md: ${err.message}`));
else console.error(` ✗ copilot-instructions.md: ${err.message}`);
}
} else {
for (const skill of SKILLS) {
const spinner = ora ? ora({ text: skill.name, prefixText: " " }).start() : null;
try {
installSkill(skill, baseDir, target.skillFile);
if (spinner) spinner.succeed(c.green("✓ ") + c.bold(skill.name) + ` ${c.dim(skill.description)}`);
else console.log(` ✓ ${skill.name}`);
} catch (err) {
if (spinner) spinner.fail(c.red(`✗ ${skill.name}: ${err.message}`));
else console.error(` ✗ ${skill.name}: ${err.message}`);
}
}
}
}
console.log(`\n ${c.bold("Done.")} Open your React Native / Expo project in your IDE and run:\n`);
console.log(` ${c.cyan("/cometchat")}\n`);
}
main().catch((err) => {
console.error(chalk ? chalk.red(`\n Error: ${err.message}`) : `\n Error: ${err.message}`);
process.exit(1);
});
process.exit(result.status ?? 0);
+8
-16
{
"name": "@cometchat/skills-native",
"version": "1.0.1",
"version": "2.0.0",
"publishConfig": {
"access": "public"
},
"description": "AI coding skills for CometChat React Native UI Kit — works with Claude Code, Cursor, Kiro, and Copilot. Teaches AI agents how to integrate chat, voice, and video calling into any Expo or bare React Native app.",
"description": "Deprecated alias for @cometchat/skills@4+. Forwards `cometchat-skills-native add` to `cometchat-skills add --family native`. Update your scripts to use the unified package.",
"keywords": [
"claude-code",
"claude-skill",
"deprecated",
"cometchat",
"skills",
"react-native",
"expo",
"chat",
"ui-kit",
"ai-skills"
"alias"
],

@@ -25,3 +22,3 @@ "author": "CometChat",

},
"homepage": "https://www.cometchat.com/docs/ui-kit/react-native/overview",
"homepage": "https://github.com/cometchat/cometchat-skills#install",
"bugs": {

@@ -34,4 +31,3 @@ "url": "https://github.com/cometchat/cometchat-skills/issues"

"files": [
"bin/",
"skills/"
"bin/"
],

@@ -41,9 +37,5 @@ "engines": {

},
"scripts": {
"prepack": "node scripts/copy-skills.js"
},
"dependencies": {
"chalk": "^4.1.2",
"ora": "^5.4.1"
"chalk": "^4.1.2"
}
}
---
name: cometchat-native-bare-patterns
description: "Integration patterns for bare React Native CLI projects — pod install, Info.plist + AndroidManifest permissions, Apple privacy manifest, native module linking, Metro config."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.77; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native bare cli pods native-modules privacy-manifest"
---
## Purpose
Teaches Claude how to integrate CometChat into a bare React Native CLI project. Covers:
- Installing the full peer-dependency set + native-module autolinking
- `pod install` cadence for iOS
- Editing `ios/<AppName>/Info.plist` for iOS permissions
- Editing `android/app/src/main/AndroidManifest.xml` for Android permissions
- Android-specific async-storage Maven repo gotcha
- **Apple privacy manifest** (`ios/<AppName>/PrivacyInfo.xcprivacy`) — required for App Store compliance
- Wiring `index.js` + `App.tsx` with the provider chain
**Read `cometchat-native-core` first** (init/login/wrapper chain + anti-patterns), then `cometchat-native-components`, then `cometchat-native-placement`.
Ground truth: `docs/ui-kit/react-native/react-native-cli-integration.mdx`, `apple-privacy-manifest-guide.mdx`, `react-native-conversation.mdx` + `react-native-one-to-one-chat.mdx` + `react-native-tab-based-chat.mdx`, and `examples/SampleApp/`.
---
## Use this skill when
- Project has `ios/` and `android/` folders at the root
- `package.json` `main` is `index.js` (classic RN entry)
- No `expo` in `package.json` dependencies
- User says "React Native CLI", "bare RN", "ejected Expo", or "custom native modules"
**Do NOT use this skill when:**
- Project has `expo` in dependencies + `app.json`/`app.config.js` → use `cometchat-native-expo-patterns`
- Project is Expo that's prebuilt into `ios/` + `android/` folders → this can go either way. If the user's workflow is "I edit app.json and run `expo prebuild`", stay on expo-patterns. If they've fully committed to bare (deleted `app.json`, edit `Info.plist` directly), this skill applies.
---
## Prerequisites
- Xcode 15+ for iOS (required for Apple privacy manifest)
- Android Studio with SDK 34+
- CocoaPods installed (`brew install cocoapods` on macOS)
- `react-native-cli` or `@react-native-community/cli` usable via `npx`
- React Native **>=0.77** — older versions may work but are not officially supported by the UI Kit
---
## Step 1 — Install dependencies
```bash
# Core SDK + UI Kit
npm install @cometchat/chat-sdk-react-native
npm install @cometchat/chat-uikit-react-native
# Required peer deps (natively linked)
npm install \
@react-native-async-storage/async-storage \
@react-native-clipboard/clipboard \
@react-native-community/datetimepicker \
react-native-gesture-handler \
react-native-localize \
react-native-safe-area-context \
react-native-svg \
react-native-video
# dayjs + punycode — no native code but required
npm install dayjs punycode
```
Bare RN uses autolinking, so no `react-native link` step is needed. Just confirm everything installed cleanly — if `npm install` errored mid-way, native modules won't be wired up correctly.
### Optional — calling SDK
Only if the user's flow includes voice / video calls:
```bash
npm install \
@cometchat/calls-sdk-react-native \
@react-native-community/netinfo \
react-native-background-timer \
react-native-callstats \
react-native-webrtc
```
WebRTC bloats the binary. Skip until the user actually wants calls.
---
## Step 2 — iOS: pod install + Info.plist + PrivacyInfo
### 2a. Pod install
After **every** `npm install` of a native module (including the initial install above), run:
```bash
cd ios && pod install && cd ..
```
Without this, Xcode will fail to build with "module not found" errors for native classes. The warning signs:
- `No such module 'RNGestureHandler'` during build
- `Undefined symbol: _OBJC_CLASS_$_RNCAsyncStorage` during linking
- Build succeeds but runtime crash: "TurboModuleRegistry.getEnforcing(...): 'RNAsyncStorage' could not be found"
If pod install fails, see `cometchat-native-troubleshooting` § iOS pod install failures.
### 2b. Info.plist permissions
Open `ios/<AppName>/Info.plist` and add:
```xml
<key>NSCameraUsageDescription</key>
<string>Allow camera access to send photos and make video calls</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow microphone access to send voice messages and make calls</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow photo library access to send photos</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Allow saving photos from chat to your library</string>
```
**Permission-string best practice**: the `Usage` strings show in the system prompt when iOS asks the user for permission — write them as user-facing copy, not developer notes. `"Camera access for video calls"` is fine; `"for media upload"` isn't a real reason a user would accept.
**Merge, don't replace.** The user may have existing permission strings for other libraries — add only what's missing, don't wipe the file.
### 2c. Apple Privacy Manifest — `PrivacyInfo.xcprivacy`
**Required for App Store submission since iOS SDK 17 / Xcode 15.** If it's missing or incomplete, App Store Connect rejects the upload.
Create `ios/<AppName>/PrivacyInfo.xcprivacy` with this exact content:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
```
These 4 reason codes cover the native APIs React Native itself + the UI Kit's `react-native-video` dependency use:
| API category | Reason code | What it's for |
|---|---|---|
| `NSPrivacyAccessedAPICategoryFileTimestamp` | `C617.1` | File-modified timestamps (RN bundler) |
| `NSPrivacyAccessedAPICategoryUserDefaults` | `CA92.1` | AsyncStorage (UserDefaults backend on iOS) |
| `NSPrivacyAccessedAPICategorySystemBootTime` | `35F9.1` | Uptime for scheduling (RN + video cache) |
| `NSPrivacyAccessedAPICategoryDiskSpace` | `E174.1` | Free-space check (media upload guard) |
After adding:
1. Open `ios/<AppName>.xcworkspace` in Xcode
2. Right-click the app folder in the navigator → "Add Files to \"\<AppName\>\""
3. Select `PrivacyInfo.xcprivacy` — make sure "Add to targets: \<AppName\>" is checked
4. Rebuild
**If the user already has a `PrivacyInfo.xcprivacy`**, merge the 4 API types into their existing `NSPrivacyAccessedAPITypes` array — don't replace the whole file.
### 2d. Install pods after Info.plist / PrivacyInfo changes
```bash
cd ios && pod install && cd ..
```
---
## Step 3 — Android: AndroidManifest + Maven repo
### 3a. AndroidManifest permissions
Open `android/app/src/main/AndroidManifest.xml` and add inside `<manifest>` (before `<application>`):
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Android 12 and below -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
```
**Merge, don't replace.** Keep the user's existing permissions for other libraries.
### 3b. Android: async-storage Maven repo (REQUIRED)
`@react-native-async-storage/async-storage` v3+ ships a **local Maven artifact** that autolinking can't find by default. Without this fix, `./gradlew assembleDebug` fails with:
```
Could not find :react-native-async-storage_async-storage: on any of the paths.
```
Add the local Maven repo to `android/build.gradle`:
```gradle
allprojects {
repositories {
google()
mavenCentral()
// Required for @react-native-async-storage/async-storage v3+
maven {
url = uri(project(":react-native-async-storage_async-storage").file("local_repo"))
}
}
}
```
Without this fix, the whole Android build fails early. This is a UI Kit-specific gotcha because the kit pins async-storage v3+.
### 3c. Android: Metro config for custom fonts or assets (if applicable)
If the project uses custom icon fonts or bundled assets, confirm `react-native.config.js` includes:
```js
module.exports = {
assets: ["./src/assets/fonts/"], // only if the user has custom fonts
};
```
Run `npx react-native-asset` to link. Not required for the UI Kit itself — only relevant if the user extends with custom icons.
---
## Step 4 — Wire `index.js` + `App.tsx` with the provider chain
### 4a. `index.js` — gesture handler FIRST
```js
// index.js
import "react-native-gesture-handler"; // MUST be the first import
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => App);
```
**The `react-native-gesture-handler` import must be line 1.** Not line 2. Not after React. Without it, swipe gestures on the composer and bottom sheets silently break — often only in release builds, which makes it hard to catch during development.
### 4b. `App.tsx` — provider wrapper chain
```tsx
// App.tsx
import React from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
import { CometChatProvider } from "./src/providers/CometChatProvider";
import { AppNavigator } from "./src/navigation/AppNavigator";
import Config from "react-native-config"; // or read from an env source
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider>
<CometChatProvider
appId={Config.COMETCHAT_APP_ID!}
region={Config.COMETCHAT_REGION!}
authKey={Config.COMETCHAT_AUTH_KEY!}
uid="cometchat-uid-1" // dev mode only
>
<AppNavigator />
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
```
The `CometChatProvider` itself lives in `src/providers/CometChatProvider.tsx` per `cometchat-native-core` § 6 — reuse that implementation.
---
## Step 5 — Env vars
Bare RN has no built-in env var system. Pick one:
### Option A — `react-native-config` (most common)
```bash
npm install react-native-config
cd ios && pod install && cd ..
```
Create `.env` at project root:
```
COMETCHAT_APP_ID=your_app_id
COMETCHAT_REGION=us
COMETCHAT_AUTH_KEY=your_auth_key
```
Read via:
```tsx
import Config from "react-native-config";
const appId = Config.COMETCHAT_APP_ID;
```
**iOS post-setup**: Xcode needs to know about the `.env` file. Either use `react-native-config`'s Xcode build-phase script (documented in its README) or create a `.xcconfig` file. Without this, `Config.*` returns `undefined` on iOS.
### Option B — `babel-plugin-dotenv-import`
Simpler but less mature. Only if the user's already picked this.
### Option C — hardcoded constants (NEVER for production)
For a quick dev-mode proof-of-concept only, just declare the values as constants in a dedicated config file:
```ts
// src/config/cometchat.ts
export const CONFIG = {
APP_ID: "YOUR_APP_ID",
REGION: "us",
AUTH_KEY: "YOUR_AUTH_KEY",
};
```
Add `src/config/cometchat.ts` to `.gitignore`. Obviously do not commit real credentials.
### Never put `REST_API_KEY` in the client
The REST API key is server-only. Production user management + auth token minting happens on a backend you control — see `cometchat-native-production`.
---
## Step 6 — Run + verify
### First run
```bash
# iOS
npx react-native run-ios
# Android
npx react-native run-android
```
First build takes several minutes. Subsequent runs with the Metro bundler already running are fast.
### Verify
1. `npx tsc --noEmit` — TypeScript check
2. Open the chat screen in the simulator
3. Tap the composer — keyboard should open smoothly
4. Tap the "+" attachment button — action sheet should slide up (gesture handler)
5. Send a message — it should appear immediately
If any of these fail, see `cometchat-native-troubleshooting`.
### Re-run cadence
| Change | Command |
|---|---|
| JSX / TS changes | Fast Refresh handles it; `r` in Metro to reload |
| Added / removed a dep without native code | Restart Metro |
| Added / removed a native module | `cd ios && pod install && cd ..` then run-ios |
| Changed `Info.plist` | `run-ios` (Xcode picks up changes) |
| Changed `AndroidManifest.xml` | `run-android` |
| Changed `android/build.gradle` | `./gradlew clean` then `run-android` |
---
## Hard rules
1. **`pod install` is mandatory after every native dep change.** Missing this → build fails with "module not found" or crashes at runtime with "TurboModuleRegistry could not find X".
2. **`import "react-native-gesture-handler"` is the FIRST line of `index.js`.** Before React. Before any other import. Missing → swipe gestures silently break (often only in release builds).
3. **Android requires the local Maven repo entry** for `@react-native-async-storage/async-storage` v3+ (see § 3b). Without it, the Android build fails at the assembly step.
4. **The Apple Privacy Manifest is required for App Store submission.** Skip it and your app gets rejected. Either create `PrivacyInfo.xcprivacy` (§ 2c) or merge the 4 API types into an existing one.
5. **Merge permissions into `Info.plist` and `AndroidManifest.xml`, don't replace.** Wiping out the user's existing permissions breaks their other features.
6. **The four-wrapper chain is at the app root (`App.tsx`), not per-screen.** Re-wrapping per screen causes duplicate init + login, dropped WebSockets, and a 2-3 second flicker on first mount.
7. **`REST_API_KEY` is never in the client.** Use an external backend for token minting — see `cometchat-native-production`.
8. **Every `<CometChatMessageList>` must include `hideReplyInThreadOption`** unless you're wiring a full thread panel (see `cometchat-native-placement` § Hard rule 5).
---
## Common questions
**Q: "TurboModuleRegistry.getEnforcing(...): 'RNAsyncStorage' could not be found"**
Forgot `pod install`. Run `cd ios && pod install && cd ..` then rebuild.
**Q: Android build fails with "Could not find :react-native-async-storage_async-storage:"**
See § 3b — add the local Maven repo entry to `android/build.gradle`.
**Q: App Store rejected the build with "ITMS-91053: Missing API declaration"**
Apple Privacy Manifest is incomplete. Use the full 4-API-type declaration in § 2c.
**Q: Composer swipe gestures don't work in production**
`import "react-native-gesture-handler"` isn't the first line of `index.js`. Move it above all other imports.
**Q: `CometChatUIKit.login({uid: "..."})` resolves but components don't render**
You're probably rendering before `init()` completes. Gate rendering on `CometChatProvider`'s `isReady` state — see `cometchat-native-core` § 6.
**Q: I want to support Expo AND bare in the same codebase**
Use Expo + `npx expo prebuild` to generate native projects on demand. Stay on `cometchat-native-expo-patterns`.
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Init / login / wrapper chain / anti-patterns |
| `cometchat-native-components` | Component prop reference |
| `cometchat-native-placement` | Where chat goes (stack / tabs / modal / bottom sheet / embedded) |
| `cometchat-native-expo-patterns` | Expo managed workflow |
| `cometchat-native-bare-patterns` | This skill — bare RN CLI |
| `cometchat-native-features` | Calls, extensions, AI |
| `cometchat-native-theming` | Theme customization |
| `cometchat-native-customization` | Text formatters, events, custom views |
| `cometchat-native-production` | Server-side auth tokens + user management |
| `cometchat-native-troubleshooting` | pod install fails, build errors, missing modules, privacy manifest rejection |
---
name: cometchat-native-components
description: "Component catalog for the CometChat React Native UI Kit v5 — names, props, slot views, request builders, hide flags, style shape. Always loaded before writing CometChat* JSX."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native components catalog props"
---
## Purpose
Teaches Claude every component the React Native UI Kit exports, with the props, callback signatures, slot views, request builders, and style shapes that actually exist. This is the authoritative reference — never invent component names or props from memory; look them up here.
**Read this skill before writing any `<CometChat*>` JSX.**
Ground truth: `packages/ChatUiKit/src/index.ts` from the UI Kit source + `docs/ui-kit/react-native/components-overview.mdx` + per-component doc pages.
---
## How to use this catalog
The React Native UI Kit is a set of independent components that you compose into chat layouts. Three patterns cover almost every use case:
| Pattern | Components |
|---|---|
| **Two-pane** (inbox) | `CometChatConversations` + `CometChatMessageHeader` + `CometChatMessageList` + `CometChatMessageComposer` |
| **Single thread** (1-to-1) | `CometChatMessageHeader` + `CometChatMessageList` + `CometChatMessageComposer` (with a resolved `user` or `group`) |
| **Tab-based messenger** | `CometChatConversations` + `CometChatUsers` + `CometChatGroups` + `CometChatCallLogs` in a bottom-tab bar |
**Data flow** (identical across all 3): a list component emits a `CometChat.Conversation` / `User` / `Group` via `onItemPress`. Extract the entity (`conversation.getConversationWith()` for conversations) and pass it as a prop to the header / list / composer.
All components share the same API surface — see § Prop conventions.
---
## Prop conventions (applies to every `<CometChat*>` component)
Four prop families you'll see across the catalog:
| Family | Shape | Example |
|---|---|---|
| **Callback** | `on<Event>={(param) => void}` | `onItemPress={(conv) => ...}` • `onError={(err) => ...}` • `onSendButtonPress={(msg) => ...}` |
| **Request builder** | `<entity>RequestBuilder={new CometChat.<Entity>RequestBuilder()}` | `conversationsRequestBuilder={new CometChat.ConversationsRequestBuilder().setLimit(20)}` |
| **Hide / visibility toggle** | `hide<Feature>={boolean}` \| `<feature>Visibility={boolean}` | `hideReceipts={true}` • `hideReplyInThreadOption={true}` |
| **View slot (replace a section)** | `<Slot>View={(params) => JSX}` — **PascalCase**, returns JSX | `TitleView={(user, group) => <MyTitle />}` • `LeadingView={(u, g) => <MyAvatar />}` |
| **Style** | `style={{ containerStyle: {}, itemStyle: { ... } }}` | see § 13 Style shape |
**On-events take positional args, not an event object.** `onItemPress={(conversation) => ...}` receives the `CometChat.Conversation` directly — no `event.target`.
**Slot views are capitalized**: `TitleView`, `SubtitleView`, `LeadingView`, `TrailingView`, `EmptyStateView`, `ErrorStateView`, `LoadingStateView`, `AuxiliaryButtonView`. Each slot function gets the same props the default view would have (usually `user, group` or a single entity).
**Style is nested objects.** Top-level `style` accepts `containerStyle` (outermost wrapper) then component-specific keys. Each inner style is a regular React Native `StyleSheet` object.
---
## 1. Lists
All four list components take a request builder, an `onItemPress` callback, `on<List>LongPress` for long-press, and `style={}`.
### CometChatConversations
Scrollable list of recent conversations (both user and group).
```tsx
import { CometChatConversations } from "@cometchat/chat-uikit-react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
<CometChatConversations
conversationsRequestBuilder={
new CometChat.ConversationsRequestBuilder().setLimit(20)
}
onItemPress={(conversation) => {
const entity = conversation.getConversationWith(); // User | Group
const type = conversation.getConversationType(); // "user" | "group"
// navigate / open panel with `entity`
}}
onError={(err) => console.error(err)}
hideHeader={false}
hideReceipts={false}
style={{ containerStyle: { backgroundColor: "#fff" } }}
/>
```
Key props: `conversationsRequestBuilder`, `onItemPress`, `onItemLongPress`, `onError`, `onEmpty`, `hideReceipts`, `hideHeader`, `hideSearch`, `TitleView`, `SubtitleView`, `LeadingView`, `TrailingView`, `EmptyStateView`, `ErrorStateView`, `LoadingStateView`, `BackdropView`, `style`.
### CometChatUsers
```tsx
<CometChatUsers
usersRequestBuilder={new CometChat.UsersRequestBuilder().setLimit(30)}
onItemPress={(user) => openChatWith(user)}
searchKeyword=""
hideStatus={false}
/>
```
Key props: `usersRequestBuilder`, `onItemPress`, `onError`, `onEmpty`, `searchKeyword`, `hideStatus`, `hideSearch`, `LeadingView`, `TitleView`, `SubtitleView`, `EmptyStateView`, `ErrorStateView`, `LoadingStateView`, `style`.
### CometChatGroups
```tsx
<CometChatGroups
groupsRequestBuilder={
new CometChat.GroupsRequestBuilder().setLimit(30).joinedOnly(true)
}
onItemPress={(group) => openGroupChat(group)}
/>
```
Key props: same shape as Users, with `groupsRequestBuilder` instead.
### CometChatGroupMembers
```tsx
<CometChatGroupMembers
group={selectedGroup}
groupMemberRequestBuilder={
new CometChat.GroupMembersRequestBuilder(selectedGroup.getGuid()).setLimit(30)
}
onItemPress={(member) => openMemberDetails(member)}
hideKickMemberOption={false}
hideBanMemberOption={false}
/>
```
Key props: `group` (**required** — pass a `CometChat.Group` instance), `groupMemberRequestBuilder`, `onItemPress`, `onError`, `onBack`, `hideKickMemberOption`, `hideBanMemberOption`, `hideChangeScopeOption`, slot views for each row section, `style`.
---
## 2. Messages
### CometChatMessageHeader
```tsx
<CometChatMessageHeader
user={selectedUser} // OR group — never both
hideBackButton={false}
onBack={() => navigation.goBack()}
AuxiliaryButtonView={(user, group) => <CometChatCallButtons user={user} group={group} />}
TitleView={(user, group) => <CustomTitle />}
SubtitleView={(user, group) => <CustomSubtitle />}
/>
```
Key props: `user` OR `group` (one required), `hideBackButton`, `hideVideoCallButton`, `hideVoiceCallButton`, `onBack`, `TitleView`, `SubtitleView`, `LeadingView`, `TrailingView`, `AuxiliaryButtonView`, `BackButtonIconImageResource`, `style`.
### CometChatMessageList
Scrollable message feed. Handles reactions, receipts, mentions, threads, and media out of the box.
```tsx
<CometChatMessageList
user={selectedUser}
messageRequestBuilder={
new CometChat.MessagesRequestBuilder()
.setUID(selectedUser.getUid())
.setLimit(30)
}
hideReplyInThreadOption={true} // SEE HARD RULE § 11
hideReceipts={false}
onThreadRepliesPress={(message, bubbleView) => openThreadPanel(message)}
onError={(err) => console.error(err)}
EmptyStateView={() => <Text>No messages yet</Text>}
style={{ containerStyle: { backgroundColor: "#fff" } }}
/>
```
Key props: `user` OR `group`, `parentMessageId` (for thread replies), `messageRequestBuilder`, `goToMessageId` (scroll-to-message), `searchKeyword` (highlight in bubbles), `textFormatters`, `templates` (custom message type rendering — `CometChatMessageTemplate[]`), `hideReplyInThreadOption` **see hard rule § 11**, `hideReceipts`, `hideReactions`, `hideReplyOption`, `hideEditMessageOption`, `hideDeleteMessageOption`, `hideTranslateMessageOption`, `hideMessagePrivatelyOption`, `hideDateSeparator`, `onThreadRepliesPress`, `onMessageLongPress`, `onError`, all `*StateView` slots, `style`.
### CometChatMessageComposer
Rich text input. Attachments, mentions, voice notes, sticker picker, reaction keyboard.
```tsx
<CometChatMessageComposer
user={selectedUser}
placeholderText="Type a message..."
onSendButtonPress={(message) => console.log("sent", message)}
onError={(err) => console.error(err)}
disableMentions={false}
textFormatters={[
new CometChatMentionsFormatter(),
new CometChatUrlsFormatter(),
]}
AuxiliaryButtonView={() => <CustomAuxButton />}
attachmentOptions={(user, group) => [ /* CometChatMessageComposerAction[] */ ]}
/>
```
Key props: `user` OR `group`, `parentMessageId` (for thread composer), `placeholderText`, `onSendButtonPress`, `onError`, `onTextChanged`, `disableMentions`, `disableSoundForMessages`, `textFormatters`, `attachmentOptions`, `AuxiliaryButtonView`, `HeaderView`, `SendButtonView`, `VoiceRecordingView`, `AttachmentIconView`, `EmojiIconView`, `style`.
### CometChatCompactMessageComposer
Compact variant for small screens. Auto-expanding input, rich-text, attachments.
```tsx
<CometChatCompactMessageComposer
user={selectedUser}
enableRichTextEditor={true}
onSendButtonPress={(message) => {}}
/>
```
Use this instead of `CometChatMessageComposer` in drawers, widgets, or embedded placements. Same prop family.
### CometChatThreadHeader
Header for a threaded reply view — parent message + reply count + close.
```tsx
<CometChatThreadHeader
parentMessage={threadParent}
onClose={() => setThreadMessage(null)}
hideReplyCount={false}
/>
```
**Threading composition:**
```tsx
// In the main message list, capture a thread-open request
<CometChatMessageList
user={selectedUser}
onThreadRepliesPress={(message) => setThreadMessage(message)}
/>
// When a thread is open, render the thread panel
{threadMessage && (
<>
<CometChatThreadHeader
parentMessage={threadMessage}
onClose={() => setThreadMessage(null)}
/>
<CometChatMessageList
user={selectedUser}
parentMessageId={threadMessage.getId()}
/>
<CometChatMessageComposer
user={selectedUser}
parentMessageId={threadMessage.getId()}
/>
</>
)}
```
---
## 3. Calling (separate SDK)
Call components live in `@cometchat/chat-uikit-react-native` but **require `@cometchat/calls-sdk-react-native` to be installed** to work. Don't import any of these if the calls SDK isn't in the project.
### CometChatCallButtons
Voice + video call initiators. Drop into `AuxiliaryButtonView` on `CometChatMessageHeader` for phone + camera icons next to a user's name.
```tsx
<CometChatCallButtons
user={selectedUser}
onVoiceCallPress={(session) => navigation.navigate("OngoingCall", { session })}
onVideoCallPress={(session) => navigation.navigate("OngoingCall", { session })}
hideVideoCallButton={false}
hideVoiceCallButton={false}
/>
```
### CometChatIncomingCall
Incoming call notification. Render at the app root so it's visible on any screen.
```tsx
<CometChatIncomingCall
call={incomingCall}
onAccept={(call) => {}}
onDecline={(call) => {}}
disableSoundForCalls={false}
/>
```
### CometChatOutgoingCall
Ringing-while-calling screen after `CometChat.initiateCall(...)`.
```tsx
<CometChatOutgoingCall
call={outgoingCall}
onClosePress={() => {}}
/>
```
### CometChatOngoingCall
In-call UI — tiles, controls, mute, end-call.
```tsx
<CometChatOngoingCall
sessionID={session.sessionId}
callType="audio" // or "video"
onCallEnded={() => navigation.goBack()}
/>
```
### CometChatCallLogs
Scrollable call history.
```tsx
<CometChatCallLogs
callLogsRequestBuilder={/* CallLogRequest builder from calls-sdk */}
onItemPress={(callLog) => openCallDetails(callLog)}
/>
```
### CometChatMeetCallBubble
Call-event message bubble. Auto-picked up by the message list — you don't render it manually.
**Wiring**: requires `CallingExtension` to be initialized before `CometChatUIKit.init` (handled by the calls-sdk auto-init), and `<CometChatIncomingCall>` mounted at the app root. See `cometchat-native-features` § Calls.
---
## 4. AI
### CometChatAIAssistantChatHistory
AI assistant conversation history UI.
```tsx
<CometChatAIAssistantChatHistory
user={loggedInUser}
onMessageClicked={(message) => openChat(message)}
onNewChatButtonClick={() => startNewChat()}
/>
```
Separate dashboard setup required to enable AI agents. See `cometchat-native-features` § AI agent.
---
## 5. Search
### CometChatSearch
Full-featured search across conversations + messages + users + groups. Scoped to a user/group when passed a target.
```tsx
<CometChatSearch
uid={selectedUser?.getUid()} // optional — scope to one user's chat
guid={selectedGroup?.getGuid()} // optional — scope to one group
onBack={() => setShowSearch(false)}
onConversationPress={(conv) => openConversation(conv)}
onMessagePress={(msg) => scrollToMessage(msg)}
/>
```
Key props: `uid` / `guid` (scope), `searchKeyword`, `onBack`, `onConversationPress`, `onMessagePress`, `onUserPress`, `onGroupPress`, `hideConversations`, `hideMessages`, `hideUsers`, `hideGroups`, `style`.
Wire from `CometChatMessageHeader`'s `onSearchPress`.
---
## 6. Atoms (primitives for custom composition)
Building blocks the higher-level components use internally. Use inside `<Slot>View` overrides or custom screens.
| Component | Purpose |
|---|---|
| `CometChatAvatar` | Circular / rounded avatar. `image`, `name` (initials fallback), `backgroundColor`, `size` |
| `CometChatBadge` | Small pill badge — unread count, typing indicator, labels |
| `CometChatStatusIndicator` | Online/offline dot. `status`, `borderColor` |
| `CometChatListItem` | Standard row — leading view + title/subtitle + trailing view |
| `CometChatDate` | Relative-time date pill. `timestamp`, `pattern` |
| `CometChatBottomSheet` | Modal sheet from bottom. Imperative `show()` / `hide()` via ref |
| `CometChatActionSheet` | iOS-style action-sheet list |
| `CometChatConfirmDialog` | Standard confirm / cancel dialog |
| `CometChatReportDialog` | Report-user dialog |
| `CometChatEmojiKeyboard` | Full emoji picker |
| `CometChatMediaRecorder` | Voice-note recorder UI |
| `CometChatInlineAudioRecorder` | Inline variant used inside the composer |
| `CometChatReactions` | Reaction-bar UI |
| `CometChatReactionList` | Full reaction-list popup |
| `CometChatQuickReactions` | Quick reactions prompt (long-press) |
| `CometChatMessagePreview` | Reply-quote preview in the composer |
All atoms take `style={}` in the same nested-object shape.
---
## 7. Bubbles
The message list renders bubbles automatically based on message type. Use them directly only when you need a custom bubble template.
| Bubble | Renders |
|---|---|
| `CometChatTextBubble` | Text messages |
| `CometChatImageBubble` | Image messages |
| `CometChatAudioBubble` | Audio clip messages |
| `CometChatVideoBubble` | Video messages |
| `CometChatFileBubble` | File attachments |
| `CometChatMeetCallBubble` | Call event marker (auto) |
Extension-provided bubbles (only present if the extension is registered):
| Bubble | Extension |
|---|---|
| `CometChatStickerBubble` | Stickers |
| `LinkPreviewBubble` | Link previews |
| `MessageTranslationBubble` | Translation inline |
| `CometChatCollaborativeDocumentBubble` | Collab doc |
| `CometChatCollaborativeWhiteBoardBubble` | Collab whiteboard |
---
## 8. Formatters (custom text rendering)
Pass via `CometChatMessageList.textFormatters` or the composer's same prop.
| Formatter | Transforms |
|---|---|
| `CometChatMentionsFormatter` | `@uid` → linked mention bubble |
| `CometChatUrlsFormatter` | URLs → tappable links |
| `CometChatRichTextFormatter` | Markdown-ish `**bold**`, `*italic*`, `__underline__`, inline code |
| `CometChatTextFormatter` | Base class — extend for custom formatters |
```tsx
<CometChatMessageList
user={selectedUser}
textFormatters={[
new CometChatMentionsFormatter(),
new CometChatUrlsFormatter(),
new CometChatRichTextFormatter(),
]}
/>
```
See `cometchat-native-customization` for the `extends CometChatTextFormatter` recipe.
---
## 9. Infrastructure (static classes + event bus)
### CometChatUIKit
Static class — init + login + logout + send.
| Method | Purpose |
|---|---|
| `CometChatUIKit.init(settings)` | Initialize. Must resolve before any component renders. |
| `CometChatUIKit.login({ uid })` | Log in (dev mode). **Takes an object**, not `login("...")`. |
| `CometChatUIKit.login({ authToken })` | Log in with a server-minted token (production). Same method as dev, different arg. |
| `CometChatUIKit.getLoggedinUser()` | Returns current `CometChat.User` or `null` (Promise). |
| `CometChatUIKit.logout()` | Log out + clear session. |
| `CometChatUIKit.sendCustomMessage(msg)` | Send custom message (used by calling, extensions). |
| `CometChatUIKit.uiKitSettings` | Read back the settings passed to `init()`. |
### UIKitSettingsBuilder
Fluent builder for the `init()` settings. See `cometchat-native-core` § 1.
### CometChatUIEventHandler
Event bus. Subscribe to UI events emitted by components.
```tsx
import { CometChatUIEventHandler } from "@cometchat/chat-uikit-react-native";
const listenerId = "MY_LISTENER_" + Date.now();
CometChatUIEventHandler.addMessageListener(listenerId, {
ccMessageSent: ({ message }) => { /* ... */ },
ccMessageEdited: ({ message }) => { /* ... */ },
ccMessageDeleted: ({ message }) => { /* ... */ },
});
// Cleanup:
CometChatUIEventHandler.removeMessageListener(listenerId);
```
Also: `addConversationListener`, `addGroupListener`, `addUserListener`, `addCallListener` (all paired with `remove*Listener`).
### CometChatMessageTemplate + CometChatMessageOption
- **`CometChatMessageTemplate`**: register a custom message type. Pass via `templates` prop on `CometChatMessageList`.
- **`CometChatMessageOption`**: override or extend long-press options on a message.
See `cometchat-native-customization` for recipes.
### CometChatSoundManager
```tsx
import { CometChatSoundManager, SoundOutput } from "@cometchat/chat-uikit-react-native";
CometChatSoundManager.play(SoundOutput.incomingMessage);
```
### DataSource / DataSourceDecorator / MessageDataSource / ExtensionsDataSource / ChatConfigurator
Lower-level extension points for deep customization. `cometchat-native-customization` § Tier 4 covers when to use these instead of props.
---
## 10. Extensions (opt-in)
Extensions add message types + dashboard-toggleable features. Each ships its own bubble + composer action. Register via `UIKitSettingsBuilder.setExtensions([...])` before `init`.
| Extension | Adds |
|---|---|
| `PollsExtension` | `CometChatCreatePoll` composer action + vote-tracking bubble |
| `StickersExtension` | Sticker picker + `CometChatStickerBubble` |
| `LinkPreviewExtension` | `LinkPreviewBubble` for rich link cards |
| `MessageTranslationExtension` | Per-message translate option |
| `CollaborativeDocumentExtension` | Shared-doc composer action + bubble |
| `CollaborativeWhiteboardExtension` | Shared-whiteboard composer action + bubble |
| `ThumbnailGenerationExtension` | Auto-thumbnails for image attachments |
See `cometchat-native-features` for when each requires a dashboard toggle vs. just-install-and-go.
---
## 11. Hard rule — `hideReplyInThreadOption` is mandatory on every MessageList
**Every `<CometChatMessageList>` MUST include `hideReplyInThreadOption`** unless the integration also wires a full thread panel (`CometChatThreadHeader` + scoped `CometChatMessageList` with `parentMessageId` + scoped `CometChatMessageComposer` with `parentMessageId`).
The kit's default (`hideReplyInThreadOption: false`) puts a "Reply in Thread" entry in the message action menu that silently does nothing when no panel is wired. In drawer / widget / modal / stack-screen integrations without a thread panel, the option is a dead click.
Every example in this catalog includes the flag for a reason — keep it. The only place you can omit it is inside a full thread-panel composition (see § 2 Threading).
---
## 12. Common prop-finding recipe
When a user's request isn't obviously covered, check in this order before writing custom code:
1. **Named component in this catalog fits?** ("show call history" → `CometChatCallLogs`).
2. **A `hide*` / visibility prop?** ("hide receipts" → `hideReceipts={true}`, not custom bubbles).
3. **A `<Slot>View` prop?** ("customize the header title" → `TitleView={(u, g) => <MyTitle />}`, not a wholesale header replacement).
4. **A `*RequestBuilder`?** ("filter conversations" → `conversationsRequestBuilder` with `.setUserTags([...])`, not post-render filtering).
5. **`textFormatters` + `templates`** for message-rendering customization.
6. **`CometChatUIEventHandler`** for cross-component communication.
7. **Only then** escalate to `cometchat-native-customization` § Tier 4 (DataSource decorators, ChatConfigurator).
Never hand-roll a bubble, a header, or a list when the kit ships one — you'll miss theming, reactions, typing indicators, receipts, and cross-framework behavior the built-ins handle.
---
## 13. Style shape reference
Every style prop follows a nested-object shape. Pass `style={}` for no customization; override only the keys you care about.
```tsx
<CometChatMessageList
style={{
containerStyle: { backgroundColor: "#fff", flex: 1 },
headerStyle: { titleStyle: { color: "#000" } },
avatarStyle: { containerStyle: { borderRadius: 8 } },
dateStyle: { textStyle: { fontSize: 10 } },
}}
/>
```
Common style keys across components:
- `containerStyle` — outermost wrapper
- `itemStyle` — individual list rows
- `headerStyle`, `titleStyle`, `subtitleStyle`
- `avatarStyle`, `badgeStyle`, `statusIndicatorStyle`
- `bubbleStyle` (message list), `composerStyle` (composer)
- `emptyStateStyle`, `errorStateStyle`, `loadingStateStyle`
Theme-level tokens (`primary`, `textPrimary`, etc.) propagate via `CometChatThemeProvider` — prefer overriding theme tokens for app-wide changes and `style={}` only for per-component tweaks. See `cometchat-native-theming`.
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Always read first — init, login, provider chain |
| `cometchat-native-components` | This skill — any time you write `<CometChat*>` JSX |
| `cometchat-native-placement` | Deciding WHERE components go (stack / tabs / modal / sheet) |
| `cometchat-native-customization` | `textFormatters`, `templates`, custom slot views, event bus |
| `cometchat-native-features` | Adding calls, extensions, AI |
| `cometchat-native-theming` | `style={}` not enough — need app-wide color / typography changes |
| `cometchat-native-production` | `login({ authToken })` setup |
| `cometchat-native-troubleshooting` | `<CometChat*>` renders nothing or throws at runtime |
---
name: cometchat-native-core
description: "Shared rules for CometChat React Native UI Kit v5. Always loaded alongside framework (expo/bare) and placement skills. Read this first."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5; @cometchat/chat-sdk-react-native ^4"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "chat cometchat react-native core rules initialization provider theming"
---
## Purpose
This is the foundational skill for every CometChat React Native UI Kit v5 integration. It teaches Claude HOW CometChat works on RN — initialization order, provider wrapper chain, login, env vars, auth tokens, and the anti-patterns that break real apps.
**Read this skill first, before any framework (`cometchat-native-expo-patterns` / `cometchat-native-bare-patterns`) or placement skill.**
Ground-truth sources: `docs/ui-kit/react-native/overview.mdx`, `react-native-cli-integration.mdx`, `expo-integration.mdx`, `methods.mdx`, and `@cometchat/chat-uikit-react-native@5.3.3`'s `src/index.ts`.
---
## 1. The init-login-render order
CometChat has exactly one valid lifecycle on React Native:
```
CometChatUIKit.init(settings) → CometChatUIKit.login({ uid }) → render <CometChat*> components
```
Breaking this order produces a blank screen, a "CometChat is not initialized" runtime error, or a hung login. No exceptions.
### UIKitSettings — the init object
```tsx
import {
CometChatUIKit,
UIKitSettingsBuilder,
} from "@cometchat/chat-uikit-react-native";
const settings = new UIKitSettingsBuilder()
.setAppId(APP_ID) // Required — from the CometChat dashboard
.setRegion(REGION) // Required — "us" | "eu" | "in"
.setAuthKey(AUTH_KEY) // Required for dev mode. Omit in production.
.subscribePresenceForAllUsers() // Optional but recommended — online indicators
.build();
await CometChatUIKit.init(settings);
```
### Init must happen once
Use a module-level flag to prevent double-init. React re-mounts in dev (strict mode, fast refresh, and navigation nesting all trigger effect re-fires):
```tsx
let initialized = false;
async function initCometChat(): Promise<void> {
if (initialized) return;
initialized = true;
const settings = new UIKitSettingsBuilder()
.setAppId(APP_ID)
.setRegion(REGION)
.setAuthKey(AUTH_KEY)
.subscribePresenceForAllUsers()
.build();
await CometChatUIKit.init(settings);
}
```
### Init must run before first render
Put the init call in a top-level `useEffect` (preferred — the provider pattern in section 6 does this) or in `App.tsx` before the initial navigator mounts. Avoid calling `init()` in a screen's effect — by the time the screen mounts, the app has already tried to render components that expect init to be done.
---
## 2. Login
### Development mode
```tsx
const user = await CometChatUIKit.getLoggedinUser();
if (!user) {
await CometChatUIKit.login({ uid: "cometchat-uid-1" }); // note: OBJECT form
}
```
**⚠️ `login()` takes an object `{ uid: "..." }` on React Native**, not a bare string like on the web. Passing `"cometchat-uid-1"` directly silently fails.
Every new CometChat app ships 5 pre-seeded test users — `cometchat-uid-1` through `cometchat-uid-5`. Use one for development.
### ⚠️ `login()` is safe sequentially, NOT concurrently
A second `login()` call fired while the first is in-flight throws *"Please wait until the previous login request ends."* Classic trap in React Native because:
- React strict mode double-mounts effects
- `react-navigation` remounts screens on tab switches
- Fast Refresh triggers effect re-runs in dev
Guard with a module-level in-flight promise, same pattern as the web skill:
```tsx
let loginInFlight: Promise<unknown> | null = null;
async function ensureLoggedIn(uid: string, authToken?: string): Promise<void> {
const existing = await CometChatUIKit.getLoggedinUser();
if (existing) return;
if (loginInFlight) {
await loginInFlight; // reuse the pending promise
return;
}
loginInFlight = authToken
? CometChatUIKit.login({ authToken })
: CometChatUIKit.login({ uid });
try {
await loginInFlight;
} finally {
loginInFlight = null;
}
}
```
Call `ensureLoggedIn()` from the provider / effect. Both mounts resolve against the same promise; only one login request hits the server.
### Production mode
Use `CometChatUIKit.login({ authToken })` with a token from your backend. The backend generates the token with the CometChat REST API using the server-only **REST API Key** (not the client-side Auth Key). See `cometchat-native-production` for the server-side token endpoint patterns.
### Logout
```tsx
await CometChatUIKit.logout();
```
Clears the local CometChat session. Call from your app's sign-out handler.
---
## 3. Provider wrapper chain (mandatory order)
Every CometChat RN app has this wrapper chain at the root. Missing wrappers cause silent layout breakage, broken gestures, or hard crashes — each wrapper is required by a specific RN ecosystem piece the UI Kit depends on.
```tsx
// App.tsx (bare) or the root of your Expo app
import "react-native-gesture-handler"; // MUST be the first import
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider>
<CometChatProvider> {/* your own init/login provider — see section 6 */}
<AppNavigator />
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
```
Why each wrapper is mandatory:
| Wrapper | Required because |
|---|---|
| `import "react-native-gesture-handler"` (at the very top of entry) | RNGH patches the global gesture system; must happen before any screen renders. |
| `<GestureHandlerRootView style={{ flex: 1 }}>` | Message composer swipe actions, attachment sheet drags, modal swipe-to-dismiss all use RNGH. No wrapper → gestures silently disabled. |
| `<SafeAreaProvider>` | UI Kit headers + bottom-sheets respect safe-area insets. Missing → content overlaps status bar / home indicator. |
| `<CometChatThemeProvider>` | Provides the JS theme context. UI Kit components read colors / fonts / styles from here. Missing → components throw or render with fallback styles that may look broken. |
| Your own `<CometChatProvider>` | Wraps the `init` + `login` lifecycle in React state so child components can gate on `isReady`. Not optional — you can't render UI Kit components before init + login complete. |
The `cometchat-native-expo-patterns` and `cometchat-native-bare-patterns` skills show framework-specific nuances (Expo adds `expo-splash-screen`, bare adds pod setup), but the four-wrapper chain is fixed.
**Localizing to a non-English audience?** Add `<CometChatI18nProvider>` as a fifth wrapper, above `<CometChatThemeProvider>`. See `cometchat-native-theming § 9` for the full five-wrapper chain (gesture → safe-area → **i18n** → theme → provider) and localization config.
---
## 4. Environment variables
### Values to set
| Variable | Purpose | Client-exposed? |
|---|---|---|
| `APP_ID` | Dashboard App ID | Yes |
| `REGION` | `us` \| `eu` \| `in` | Yes |
| `AUTH_KEY` | Dev-mode login key — **never in production** | Yes (dev only) |
| `REST_API_KEY` | Server-side token generation — **server-only** | NO — server env only |
### Where they live
- **Bare RN**: `.env` at project root + a runtime reader like `react-native-config` or `babel-plugin-dotenv-import`. Access: `Config.APP_ID`.
- **Expo managed**: `app.json` `extra` section + read via `Constants.expoConfig?.extra?.APP_ID` from `expo-constants`. Or use `.env` with `expo-dotenv` / `expo-router`'s built-in support depending on SDK version. The `cometchat-native-expo-patterns` skill covers this in detail.
Do NOT bundle the `REST_API_KEY` into the client — RN bundles everything visible. Server endpoints live outside the RN app (Express / Hono / Cloud Functions); see `cometchat-native-production`.
### .env / extra example
```
# client-side (safe to ship in the RN bundle for dev mode)
APP_ID=your_app_id
REGION=us
AUTH_KEY=your_auth_key
# server-only (NEVER ship — used by your token endpoint)
# REST_API_KEY=your_rest_api_key
```
---
## 5. Android + iOS platform notes
The UI Kit is cross-platform, but a few concerns only apply to one target:
| Platform | Concern | Fix |
|---|---|---|
| iOS (bare) | Missing `pod install` after `npm install` | `cd ios && pod install` after adding or updating any CometChat dep |
| iOS | Apple privacy manifest (PrivacyInfo.xcprivacy) required since Xcode 15+ | See `docs/apple-privacy-manifest-guide.mdx`; copied into `cometchat-native-bare-patterns` |
| iOS | Microphone / camera / photo-library permissions in `Info.plist` for calls + media messages | `NSCameraUsageDescription`, `NSMicrophoneUsageDescription`, `NSPhotoLibraryUsageDescription` strings |
| Android | Internet + read-media permissions in `AndroidManifest.xml` | `INTERNET`, `READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO`, `RECORD_AUDIO` (only what you need) |
| Both | Push notifications require the APNs + FCM dance — not automatic | Covered by `SampleAppWithPushNotifications` + `cometchat-native-troubleshooting` |
The framework skills (`cometchat-native-expo-patterns`, `cometchat-native-bare-patterns`) apply these platform settings with the right syntax for each workflow.
---
## 6. Provider pattern
Instead of inlining `init` + `login` in every component, create a reusable `CometChatProvider` that gates rendering on `isReady`. Drop it below `<CometChatThemeProvider>` in the wrapper chain.
```tsx
// CometChatProvider.tsx
import React, { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import {
CometChatUIKit,
UIKitSettingsBuilder,
} from "@cometchat/chat-uikit-react-native";
interface CometChatContextValue {
isReady: boolean;
error: string | null;
}
const CometChatContext = createContext<CometChatContextValue>({
isReady: false,
error: null,
});
export const useCometChat = () => useContext(CometChatContext);
// Module-level state — shared across all mounts
let initialized = false;
let loginInFlight: Promise<unknown> | null = null;
async function ensureLoggedIn(uid: string, authToken?: string): Promise<void> {
const existing = await CometChatUIKit.getLoggedinUser();
if (existing) return;
if (loginInFlight) {
await loginInFlight;
return;
}
loginInFlight = authToken
? CometChatUIKit.login({ authToken })
: CometChatUIKit.login({ uid });
try {
await loginInFlight;
} finally {
loginInFlight = null;
}
}
interface CometChatProviderProps {
appId: string;
region: string;
authKey?: string;
authToken?: string;
uid?: string;
children: ReactNode;
}
export function CometChatProvider({
appId,
region,
authKey,
authToken,
uid = "cometchat-uid-1",
children,
}: CometChatProviderProps) {
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function setup() {
try {
if (!initialized) {
initialized = true;
const builder = new UIKitSettingsBuilder()
.setAppId(appId)
.setRegion(region)
.subscribePresenceForAllUsers();
if (authKey) builder.setAuthKey(authKey);
await CometChatUIKit.init(builder.build());
}
await ensureLoggedIn(uid, authToken);
setIsReady(true);
} catch (e) {
setError(String(e));
}
}
setup();
}, [appId, region, authKey, authToken, uid]);
if (error) {
return null; // or your app's error boundary — don't render CometChat components
}
if (!isReady) {
return null; // or a splash / loading screen
}
return (
<CometChatContext.Provider value={{ isReady, error }}>
{children}
</CometChatContext.Provider>
);
}
```
Children of `<CometChatProvider>` can use `useCometChat()` to check `isReady` — useful if some UI wants to render before chat is ready.
---
## 7. Anti-patterns
1. **Do NOT call `CometChatUIKit.init()` during render.** Init is async with side effects; calling during render triggers infinite re-renders. Always inside `useEffect` or before `createRoot` equivalent.
2. **Do NOT call `login("uid")` with a string.** RN's `login()` expects an object: `login({ uid: "..." })`. Passing a string silently no-ops.
3. **Do NOT skip the four-wrapper chain** (GestureHandlerRootView → SafeAreaProvider → CometChatThemeProvider → your provider). Each wrapper is required.
4. **Guard concurrent `login()` with a module-level in-flight promise.** `login()` is only safe sequentially. Two calls racing (React strict mode, tab remount, Fast Refresh) throw *"Please wait until the previous login request ends."*
5. **Do NOT hardcode `AUTH_KEY` in source files.** Use env vars for dev. Use `login({ authToken })` in production.
6. **Do NOT render CometChat components before `isReady`.** The provider's `isReady: false` branch should return `null` (or a splash), not try to render children.
7. **Do NOT re-initialize on navigation.** Init and login belong at app root, not per-screen. Re-init causes WebSocket churn and lost messages mid-switch.
8. **Do NOT invent component names.** Only use components exported from `@cometchat/chat-uikit-react-native`. See `cometchat-native-components` for the catalog.
9. **Do NOT forget `import "react-native-gesture-handler"` at the top of the entry file** (`App.tsx` or `index.js`). Without it, swipe gestures in the composer and bottom sheets silently disable.
10. **Do NOT bundle the REST API key.** It's server-only. Token generation happens on your backend; the RN client never sees it.
---
## 8. Docs MCP (recommended, not required)
The CometChat docs MCP gives runtime access to the most current RN UI Kit docs. Install:
```bash
claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp
```
Use the MCP to verify prop signatures, callback names, theme token names, or error message meanings before writing any non-obvious code. Everything the skills describe here is grounded in the docs — the MCP is how you double-check during generation.
Not required to install. The skills ship with the current truth baked in. The MCP is the fallback for edge cases and for upstream changes between skill releases.
---
## 9. Package dependencies
Minimum peer deps to install before the UI Kit works:
```bash
npm install \
@cometchat/chat-sdk-react-native \
@cometchat/chat-uikit-react-native \
react-native-gesture-handler \
react-native-safe-area-context \
react-native-reanimated
```
Expo adds `expo-av` / `expo-image-picker` depending on which features you enable. Calls require the separate package:
```bash
npm install @cometchat/calls-sdk-react-native
```
See `cometchat-native-features` for when to add the calls SDK.
---
## Skill routing reference
| Skill | When to load |
|---|---|
| `cometchat-native-core` | Always — before any integration code |
| `cometchat-native-components` | Always — before writing any `<CometChat*>` JSX |
| `cometchat-native-placement` | When integrating — for placement patterns |
| `cometchat-native-expo-patterns` | Framework = Expo managed |
| `cometchat-native-bare-patterns` | Framework = bare React Native |
| `cometchat-native-theming` | When customizing themes |
| `cometchat-native-features` | When adding features (calls / extensions / AI) |
| `cometchat-native-customization` | When customizing components (text formatters, events, DataSource) |
| `cometchat-native-production` | When setting up server-side auth + user management |
| `cometchat-native-troubleshooting` | When diagnosing build errors, runtime failures, permission issues |
---
name: cometchat-native-customization
description: "Customize the CometChat React Native UI Kit without forking — four-tier model: props → request builders → text formatters + message templates → DataSource decorators + event bus."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native customization formatters events datasource templates"
---
## Purpose
Teaches Claude how to change the behavior or appearance of the React Native UI Kit **without modifying the kit itself**. Four tiers, from cheapest to deepest:
```
Tier 1 — Props (95% of asks solved here)
Tier 2 — RequestBuilder (filter what data loads)
Tier 3 — Formatters + Templates (change how text / messages render)
Tier 4 — DataSource decorators + Events (last resort, powerful)
```
**Always try Tier 1 first.** Escalate only when the tier can't do what the user wants.
**Read `cometchat-native-components` first** — the catalog is the source of truth for prop names, slot views, and event listener names that this skill builds on.
Ground truth: `docs/ui-kit/react-native/custom-text-formatter-guide.mdx`, `mentions-formatter-guide.mdx`, `shortcut-formatter-guide.mdx`, `url-formatter-guide.mdx`, `events.mdx`, `methods.mdx`, `property-changes.mdx`, and the kit's source at `packages/ChatUiKit/src/shared/formatters/` and `packages/ChatUiKit/src/shared/events/`.
---
## Four-tier triage — pick the right tier before writing any code
When a user says "I want X" for a CometChat component:
| If they want to... | Use Tier | Cost |
|---|---|---|
| Hide a feature (thread option, receipts, edit, etc.) | Tier 1 — `hide*` / `*Visibility` props | 1 line of JSX |
| Customize a subsection (header title, subtitle, avatar, empty state) | Tier 1 — `<Slot>View` prop | 1 component |
| Filter what loads (only show online users, exclude blocked, include tags) | Tier 2 — `*RequestBuilder` | 1 builder |
| Change how URLs / mentions / hashtags / emojis render inline | Tier 3 — `textFormatters` | Subclass of `CometChatTextFormatter` |
| Render a custom message type (custom bubble, custom interactive msg) | Tier 3 — `templates` + `CometChatMessageTemplate` | 1 template + 1 renderer |
| React to events from another component ("they deleted a message, now reload my view") | Tier 4 — `CometChatUIEventHandler` | Listener |
| Rewrite how data flows through the kit (custom conversation sorting, override user-fetch logic) | Tier 4 — `DataSourceDecorator` | Class extension |
If a user's ask fits Tier 1 but you jumped to Tier 3, you've written 50 lines that a 1-line prop could have replaced. Start low.
---
## Tier 1 — Props (hide / slot views / styles)
`cometchat-native-components` is the full catalog. Three prop families cover most customization:
### 1a. `hide*` / `*Visibility` flags
Turn features off with a single prop:
```tsx
<CometChatMessageList
user={selectedUser}
hideReplyInThreadOption // already mandatory — see components § 11
hideReceipts
hideReactions={false}
hideTranslateMessageOption
hideMessagePrivatelyOption
hideReplyOption={false}
/>
```
Full list of `hide*` props per component: `cometchat-native-components`. Check there before writing custom code.
### 1b. `<Slot>View` props — replace a section
Every component has PascalCase slot props for replacing named sections of its default UI:
```tsx
<CometChatMessageHeader
user={selectedUser}
TitleView={(user, group) => <Text style={styles.customTitle}>{user?.getName()}</Text>}
SubtitleView={(user, group) => <OnlineStatus user={user} />}
LeadingView={(user, group) => <CustomAvatar user={user} />}
TrailingView={(user, group) => <CustomActions user={user} />}
AuxiliaryButtonView={(user, group) => <CometChatCallButtons user={user} group={group} />}
/>
```
Slot functions receive the same data the default view would have (typically `user`, `group`, or a single entity). They return RN JSX.
**For custom views that should match the theme**, use `useTheme()`:
```tsx
import { useTheme } from "@cometchat/chat-uikit-react-native";
function CustomTitle({ user }: any) {
const theme = useTheme();
return (
<Text style={{
color: theme.color.textPrimary,
fontFamily: theme.typography.heading3.fontFamily,
fontSize: theme.typography.heading3.fontSize,
}}>
{user?.getName()}
</Text>
);
}
```
See `cometchat-native-theming` § 8 for more on `useTheme()`.
### 1c. `style={{ ... }}` prop — nested styling
Each component accepts a nested-object `style` prop (see `cometchat-native-components` § 13):
```tsx
<CometChatConversations
style={{
containerStyle: { backgroundColor: "#FAFAFA" },
itemStyle: {
avatarStyle: { containerStyle: { borderRadius: 8 } },
},
}}
/>
```
Prefer theme-level changes (via `cometchat-native-theming`) for app-wide color shifts; use `style={{}}` only for one-off overrides on a single component instance.
---
## Tier 2 — RequestBuilder filtering
For "I want to show a subset of X", use the matching `*RequestBuilder`. Never post-filter in-render.
```tsx
import { CometChat } from "@cometchat/chat-sdk-react-native";
// Only conversations in a specific tag group
<CometChatConversations
conversationsRequestBuilder={
new CometChat.ConversationsRequestBuilder()
.setLimit(20)
.setUserTags(["premium"])
.setConversationType(CometChat.RECEIVER_TYPE.USER)
}
/>
// Only online users, exclude blocked
<CometChatUsers
usersRequestBuilder={
new CometChat.UsersRequestBuilder()
.setLimit(30)
.setStatus("online")
.setSearchKeyword("")
.friendsOnly(false)
}
/>
// Only groups you've joined
<CometChatGroups
groupsRequestBuilder={
new CometChat.GroupsRequestBuilder()
.setLimit(30)
.joinedOnly(true)
}
/>
// Message list — exclude system messages
<CometChatMessageList
user={user}
messageRequestBuilder={
new CometChat.MessagesRequestBuilder()
.setUID(user.getUid())
.setLimit(30)
.setCategories(["message"]) // exclude "call", "action"
.hideReplies(false)
}
hideReplyInThreadOption
/>
```
Each request builder is chainable. The `@cometchat/chat-sdk-react-native` exports the builder classes — import them from the SDK, not the UI Kit.
### Finding the right method
Request builder methods are documented at `cometchat.com/docs/sdk/react-native` (or query the docs MCP). Common ones:
| Builder | Useful methods |
|---|---|
| `ConversationsRequestBuilder` | `.setLimit(n)`, `.setUserTags([...])`, `.setGroupTags([...])`, `.setConversationType(type)`, `.withTags(true)`, `.withUserAndGroupTags(true)` |
| `UsersRequestBuilder` | `.setLimit(n)`, `.setStatus("online")`, `.setSearchKeyword(str)`, `.friendsOnly(bool)`, `.setTags([...])`, `.setUIDs([...])`, `.hideBlockedUsers(bool)` |
| `GroupsRequestBuilder` | `.setLimit(n)`, `.setSearchKeyword(str)`, `.joinedOnly(bool)`, `.setTags([...])`, `.setGroupTypes([...])` |
| `MessagesRequestBuilder` | `.setUID(uid)` / `.setGUID(guid)`, `.setLimit(n)`, `.setCategories([...])`, `.setTypes([...])`, `.hideReplies(bool)`, `.setTags([...])`, `.setParentMessageId(id)` |
| `GroupMembersRequestBuilder` | `.setLimit(n)`, `.setSearchKeyword(str)`, `.setScopes([...])` |
---
## Tier 3 — Text formatters + message templates
For "change how text or messages render", Tier 3 is the right level. Two sub-patterns:
### 3a. Custom text formatter — inline text patterns
`CometChatTextFormatter` is an abstract base class for matching inline text patterns (hashtags, keywords, emoji shortcodes, custom tags) and replacing them with custom JSX.
```tsx
import {
CometChatTextFormatter,
SuggestionItem,
} from "@cometchat/chat-uikit-react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { Text, View, StyleSheet } from "react-native";
class HashtagFormatter extends CometChatTextFormatter {
constructor() {
super();
this.setTrackingCharacter("#"); // optional — triggers suggestion list
this.setRegexPatterns([/\B#(\w+)\b/g]); // all matches get formatted
}
// Called for each bubble's text; return string | JSX
getFormattedText(
inputText: string | null | React.ReactNode,
): string | React.ReactNode {
if (typeof inputText !== "string") return inputText;
const parts = inputText.split(/(\B#\w+\b)/g);
return (
<Text>
{parts.map((part, i) =>
part.match(/^#\w+$/)
? <Text key={i} style={styles.hashtag} onPress={() => openHashtag(part)}>{part}</Text>
: <Text key={i}>{part}</Text>,
)}
</Text>
);
}
// Optional — called before a message is sent. Transform the outgoing message.
handlePreMessageSend(message: CometChat.TextMessage): CometChat.TextMessage {
// e.g. attach the list of hashtags to the message metadata
return message;
}
// Optional — for suggestion-list support (triggered by `#`)
search(searchKey: string): void {
// Fetch matching hashtags from your backend, then:
// this.setSearchData([{ id: "tag1", title: "#typescript" }]);
}
}
const styles = StyleSheet.create({
hashtag: { color: "#2563EB", fontWeight: "600" },
});
```
Register the formatter by passing it to both `CometChatMessageList` and `CometChatMessageComposer`:
```tsx
const formatters = [
new CometChatMentionsFormatter(), // keep the built-in ones
new CometChatUrlsFormatter(),
new HashtagFormatter(), // add yours
];
<CometChatMessageList
user={selectedUser}
textFormatters={formatters}
hideReplyInThreadOption
/>
<CometChatMessageComposer
user={selectedUser}
textFormatters={formatters}
/>
```
### 3b. Custom message template — entire custom bubble
For rendering a totally custom message type (interactive cards, scheduling, forms), use `CometChatMessageTemplate`.
```tsx
import {
CometChatMessageTemplate,
CometChatUiKitConstants,
} from "@cometchat/chat-uikit-react-native";
const pollTemplate = new CometChatMessageTemplate({
type: "poll",
category: CometChatUiKitConstants.MessageCategoryConstants.custom,
ContentView: (message, alignment) => (
<PollBubble message={message} alignment={alignment} />
),
BottomView: (message, alignment) => (
<PollVoteCounts message={message} />
),
options: (loggedInUser, message, group) => [
/* CometChatMessageOption[] — custom long-press menu items */
],
});
<CometChatMessageList
user={selectedUser}
templates={[pollTemplate, ...defaultTemplates]} // merge with defaults
hideReplyInThreadOption
/>
```
Getting the default templates to merge with:
```tsx
import { ChatConfigurator } from "@cometchat/chat-uikit-react-native";
const defaults = ChatConfigurator.getDataSource().getAllMessageTemplates();
<CometChatMessageList templates={[pollTemplate, ...defaults]} />
```
### When to use text formatter vs message template
| Use formatter (Tier 3a) | Use template (Tier 3b) |
|---|---|
| Change how TEXT inside a bubble renders (hashtags, URLs, mentions, emoji shortcodes) | Render a completely different bubble body |
| Content is still a `TextMessage` | Content is a custom message type (sent via `CometChat.sendCustomMessage`) |
| Doesn't need its own long-press options | Needs custom message options (vote, claim, accept, etc.) |
---
## Tier 4 — DataSource decorators + event bus
When Tiers 1-3 can't do it, you're modifying how data flows through the UI Kit. Two mechanisms:
### 4a. Event bus — `CometChatUIEventHandler`
Subscribe to events that UI Kit components emit so your own code can react.
```tsx
import { CometChatUIEventHandler } from "@cometchat/chat-uikit-react-native";
import { useEffect } from "react";
function AppScreen() {
useEffect(() => {
const listenerId = "APP_MESSAGE_LISTENER";
CometChatUIEventHandler.addMessageListener(listenerId, {
ccMessageSent: ({ message, status }) => {
// status === "inProgress" | "sent"
analytics.track("message_sent", { id: message.getId() });
},
ccMessageEdited: ({ message }) => { /* ... */ },
ccMessageDeleted: ({ message }) => { /* ... */ },
ccMessageRead: ({ message }) => { /* ... */ },
ccLiveReaction: ({ reaction }) => { /* ... */ },
});
return () => CometChatUIEventHandler.removeMessageListener(listenerId);
}, []);
return /* ... */;
}
```
### Event listener API reference
| Listener | Use when... |
|---|---|
| `addMessageListener` | reacting to any message-related event (sent, edited, deleted, read, reactions) |
| `addConversationListener` | reacting to conversation-level events (`ccConversationDeleted`, `ccUpdateConversation`) |
| `addUserListener` | reacting to user actions (`ccUserBlocked`, `ccUserUnblocked`) |
| `addGroupListener` | reacting to group lifecycle (`ccGroupCreated`, `ccGroupDeleted`, `ccGroupLeft`, `ccGroupMemberScopeChanged`, `ccGroupMemberKicked`, `ccGroupMemberBanned`, `ccGroupMemberJoined`, `ccGroupMemberAdded`, `ccOwnershipChanged`, etc.) |
| `addCallListener` | reacting to call events (`onIncomingCallAccepted`, `onCallEnded`, `onCallInitiated`, etc.) |
Every pair has a matching `remove*Listener(id)` — **always call it in the cleanup of your `useEffect`** to avoid duplicate listeners on re-render.
**Listener ID uniqueness matters.** Use a constant per component/feature. Colliding IDs cause only the latest-registered listener to fire.
### 4b. DataSource decorators
`DataSourceDecorator` and `MessageDataSource` wrap the kit's internal data source to override specific methods without forking the whole kit.
When to reach for this: overriding how user data is fetched, how conversations are sorted, adding custom message metadata to every sent message, intercepting attachment uploads.
Minimum pattern:
```tsx
import {
DataSource,
DataSourceDecorator,
ChatConfigurator,
} from "@cometchat/chat-uikit-react-native";
class MyDataSource extends DataSourceDecorator {
constructor(source: DataSource) {
super(source);
}
// Override only the method you want to change
getConversationsRequestBuilder() {
const builder = super.getConversationsRequestBuilder();
builder.setUserAndGroupTags(true);
return builder;
}
getMessageTemplate() {
const defaults = super.getMessageTemplate();
return [myCustomTemplate, ...defaults];
}
}
// Register the decorator before init — wraps the default data source
ChatConfigurator.dataSource = new MyDataSource(ChatConfigurator.getDataSource());
await CometChatUIKit.init(settings);
```
**This is an escape hatch, not a first tool.** If you find yourself reaching for Tier 4, re-check whether Tier 1 (props) or Tier 3 (templates) could have solved it. Templates + slot views cover most "custom behavior" asks.
### 4c. Extensions datasource (for extension-like deep behavior)
`ExtensionsDataSource` is the base class for registering an extension-shaped chunk of behavior (its own composer action + its own bubble + its own data handling) — this is what `PollsExtension`, `StickersExtension`, etc. extend internally. You'd only subclass this if you're shipping a reusable feature module across apps.
For a single app, use `DataSourceDecorator` instead.
---
## 5. Recipes (common customization asks → right tier)
### "Filter the conversation list to just premium users"
**Tier 2** — `conversationsRequestBuilder` with `.setUserTags(["premium"])`.
### "Custom empty state for the users list"
**Tier 1** — `EmptyStateView` slot prop on `CometChatUsers`.
### "Custom message bubble for incoming messages only"
**Tier 3b** — `CometChatMessageTemplate` with a `ContentView` that branches on `alignment === "receive"`. Or simpler — **Tier 1** `messageListStyles.receiveBubbleStyle` in the theme (see `cometchat-native-theming` § 6).
### "Show a custom view when the user types @"
**Tier 3a** — subclass `CometChatMentionsFormatter` (or extend `CometChatTextFormatter`), implement `search(key)` + `setSearchData([...])` with your own suggestion source.
### "When a message is sent, log it to our analytics"
**Tier 4a** — `CometChatUIEventHandler.addMessageListener` with `ccMessageSent` handler.
### "When a group is deleted, remove it from my local cache + navigate away"
**Tier 4a** — `addGroupListener` with `ccGroupDeleted` handler.
### "Render custom avatars for all users based on their department"
**Tier 1** — `LeadingView` slot on `CometChatConversations` + `CometChatUsers` + `CometChatMessageHeader`.
### "Disable the file attachment option"
**Tier 1** — filter the `attachmentOptions` prop on `CometChatMessageComposer`:
```tsx
<CometChatMessageComposer
user={user}
attachmentOptions={(user, group) => {
const defaults = /* default actions from ChatConfigurator */;
return defaults.filter((opt) => opt.id !== "attachment-file");
}}
/>
```
### "Show only message types that contain the word 'urgent'"
**Tier 2** — `messageRequestBuilder` with `.setSearchKeyword("urgent")`.
### "Custom message type: a 'ping' message"
**Tier 3b** — create a `CometChatMessageTemplate` with `category: "custom"` + `type: "ping"`, render a custom `ContentView`, send via `CometChat.sendCustomMessage`.
### "Completely replace the kit's conversation-loading logic"
**Tier 4b** — `DataSourceDecorator` overriding `getConversationsRequestBuilder()` + possibly wrapping the fetch itself. Rare. Try Tier 2 first.
---
## 6. Anti-patterns
1. **Don't hand-roll a bubble when a template will do.** `CometChatMessageTemplate` (Tier 3b) gives you full control over rendering + options without losing theming, reactions, typing, receipts.
2. **Don't post-filter a list's data after render.** If you want "only online users," use Tier 2 `usersRequestBuilder.setStatus("online")` — don't fetch everyone then hide rows.
3. **Don't forget to remove listeners in `useEffect` cleanup.** RN re-renders on every navigation can register duplicate listeners; each fires your handler once per registration.
4. **Don't collide listener IDs.** Use `APP_MESSAGE_LISTENER` or `${componentName}_MESSAGE_LISTENER` — constant, unique. Colliding IDs silently drop earlier registrations.
5. **Don't put `CometChatTextFormatter` instances in component state.** Construct them once at module scope (or in a `useMemo`); re-creating them on every render loses the internal suggestion state.
6. **Don't fork or patch `@cometchat/chat-uikit-react-native` directly.** Every customization should be possible via Tiers 1-4. Forking breaks on kit upgrades.
7. **Don't reach for Tier 4 before trying 1-3.** DataSource decorators are powerful but fragile to kit internal changes. Props, request builders, and templates are stable surface area.
8. **Don't change component behavior via monkey-patching (e.g., `Component.defaultProps = ...`).** Use the actual prop API. Monkey-patching is broken by design in React 19+.
---
## 7. Wiring a custom formatter end-to-end (full working example)
Say the user wants `:emoji:` shortcodes (e.g., `:smile:` → 😀):
```tsx
// 1. Define the formatter — module scope, constructed once
import { CometChatTextFormatter } from "@cometchat/chat-uikit-react-native";
import { Text } from "react-native";
const EMOJI_MAP: Record<string, string> = {
":smile:": "😀", ":heart:": "❤️", ":thumbsup:": "👍", ":fire:": "🔥",
};
class EmojiShortcodeFormatter extends CometChatTextFormatter {
constructor() {
super();
this.setRegexPatterns([/:[a-z_]+:/g]);
}
getFormattedText(input: string | null | React.ReactNode) {
if (typeof input !== "string") return input;
const parts = input.split(/(:[a-z_]+:)/g);
return (
<Text>
{parts.map((p, i) =>
EMOJI_MAP[p] ? <Text key={i}>{EMOJI_MAP[p]}</Text> : <Text key={i}>{p}</Text>,
)}
</Text>
);
}
}
// 2. Build the formatters array at module scope
import { CometChatMentionsFormatter, CometChatUrlsFormatter } from "@cometchat/chat-uikit-react-native";
export const TEXT_FORMATTERS = [
new CometChatMentionsFormatter(),
new CometChatUrlsFormatter(),
new EmojiShortcodeFormatter(),
];
// 3. Wire into list + composer (same array — must match)
import { TEXT_FORMATTERS } from "./formatters";
<CometChatMessageList user={user} textFormatters={TEXT_FORMATTERS} hideReplyInThreadOption />
<CometChatMessageComposer user={user} textFormatters={TEXT_FORMATTERS} />
```
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Init / login / provider chain |
| `cometchat-native-components` | Prop reference — which `hide*`, `<Slot>View`, `*RequestBuilder` is available (prerequisite for Tiers 1–2) |
| `cometchat-native-placement` | Where to put the customized components |
| `cometchat-native-theming` | App-wide color / typography / dark mode — Tier 1 alternative to `style={{}}` |
| `cometchat-native-features` | Which out-of-the-box features exist (so you know what needs customizing vs. what's already there) |
| `cometchat-native-customization` | This skill — four-tier triage + custom formatters / templates / DataSource / events |
| `cometchat-native-production` | When customization depends on production auth (token refresh, user-ID mapping) |
| `cometchat-native-troubleshooting` | Formatter doesn't apply, listener fires twice, slot view renders nothing, template not showing |
---
name: cometchat-native-expo-patterns
description: "Integration patterns for Expo managed workflow — app.json config, permissions, gesture handler setup, env vars, Expo Router file-based routing subsection."
license: "MIT"
compatibility: "Node.js >=18; Expo SDK >=50; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native expo managed expo-router"
---
## Purpose
Teaches Claude how to integrate CometChat into an Expo managed workflow project. Covers:
- Installing the full peer-dependency set (not just the UI Kit)
- Configuring `app.json` permissions for iOS + Android
- Wiring the provider chain in `App.tsx` with all four wrappers
- Optional calling SDK setup
- Env vars via `expo-constants` or `.env`
- **Expo Router subsection** (file-based routing)
- Prebuild + run cadence
**Read `cometchat-native-core` first** (init/login/wrapper chain + anti-patterns), then `cometchat-native-components` (prop reference), then `cometchat-native-placement` (where chat goes).
Ground truth: `docs/ui-kit/react-native/expo-integration.mdx`, `expo-conversation.mdx`, `expo-one-to-one-chat.mdx`, `expo-tab-based-chat.mdx`, and `examples/SampleAppExpo/`.
---
## Use this skill when
- Project has `expo` in `package.json` dependencies
- `app.json` / `app.config.js` exists at the root
- `package.json` `main` field references `expo` (e.g. `"main": "index.js"` with an Expo-style entry)
- The user says "Expo", "Expo Router", "managed workflow", or "EAS"
**Do NOT use this skill when:**
- The project has an `ios/` + `android/` folder at the root (that's bare RN → use `cometchat-native-bare-patterns`)
- The user says "bare React Native", "React Native CLI", or "ejected"
---
## Hard prerequisite — Expo Go is NOT supported
The CometChat UI Kit depends on native modules that can't be shimmed. This means:
- **Expo Go won't load your app** — you'll see "Main module field cannot be resolved" or similar
- You must build a **development client** (`eas build --profile development` or `expo run:ios` / `expo run:android`)
- Or install in a plain Expo simulator via prebuild
The first build can take 5-15 minutes. Subsequent runs are fast via the dev client.
Before integrating, confirm the user has either:
- `eas-cli` installed and an EAS account, OR
- Xcode + Android Studio for local prebuilds
If neither, stop and ask them to set one up. Don't waste their time installing packages that won't run.
---
## Step 1 — Install dependencies
The UI Kit has a long peer-dep tail. Install them all in one shot so Expo's resolver doesn't miss a native module during prebuild:
```bash
# Core SDK + UI Kit
npm install @cometchat/chat-sdk-react-native
npm install @cometchat/chat-uikit-react-native
# Required peer deps (all natively-linked)
npx expo install \
@react-native-async-storage/async-storage \
@react-native-clipboard/clipboard \
@react-native-community/datetimepicker \
react-native-gesture-handler \
react-native-localize \
react-native-safe-area-context \
react-native-svg \
react-native-video
# dayjs + punycode — no native code but required by the kit
npm install dayjs punycode
```
**Why `npx expo install` for the natively-linked deps?** `expo install` picks versions compatible with the project's Expo SDK. Using `npm install` directly can land incompatible versions that break prebuild.
### Optional — calling SDK
If the user's flow includes voice / video calls (the `cometchat-native-features` skill's § Calls gates this):
```bash
npm install @cometchat/calls-sdk-react-native
npx expo install \
@react-native-community/netinfo \
react-native-background-timer \
react-native-callstats \
react-native-webrtc
```
Skip these until the user actually wants calls. Adding WebRTC to an Expo project bloats the prebuild and requires extra permissions — don't speculatively enable it.
---
## Step 2 — Configure app.json
Add iOS + Android permissions so the kit's attachments, camera, mic, and media features work. Merge into existing `expo.ios.infoPlist` / `expo.android.permissions` — do not replace anything the user already has.
```json
{
"expo": {
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Allow camera access to send photos and make video calls",
"NSMicrophoneUsageDescription": "Allow microphone access to send voice messages and make calls",
"NSPhotoLibraryUsageDescription": "Allow photo library access to send photos",
"NSPhotoLibraryAddUsageDescription": "Allow saving photos from chat to your library"
}
},
"android": {
"permissions": [
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.VIBRATE",
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO",
"android.permission.READ_MEDIA_AUDIO"
]
}
}
}
```
**Permission-string best practice**: the iOS `Usage` strings show in the system prompt when iOS asks the user for permission — write them as user-facing copy, not developer notes. `"Camera access for video calls"` is fine; `"for media upload"` isn't a real reason a user would accept.
### If the project uses `app.config.js` or `app.config.ts`
Merge the same fields into the exported config. Don't switch the project from JS to JSON unless the user asks — respect their setup.
---
## Step 3 — Wire the provider chain in App.tsx
Expo projects use the same four-wrapper chain as bare RN (see `cometchat-native-core` § 3). The difference is the entry file — Expo uses `App.tsx` (or `index.ts` + `registerRootComponent`) rather than bare's `index.js` + `AppRegistry`.
```tsx
// App.tsx
import "react-native-gesture-handler"; // MUST be the first import
import React from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
import { CometChatProvider } from "./src/providers/CometChatProvider";
import { AppNavigator } from "./src/navigation/AppNavigator";
import Constants from "expo-constants";
const extra = Constants.expoConfig?.extra ?? {};
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider>
<CometChatProvider
appId={extra.COMETCHAT_APP_ID}
region={extra.COMETCHAT_REGION}
authKey={extra.COMETCHAT_AUTH_KEY}
uid="cometchat-uid-1" // dev mode only
>
<AppNavigator />
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
```
The `CometChatProvider` itself is defined per `cometchat-native-core` § 6 — reuse that implementation; don't invent another one.
### `import "react-native-gesture-handler"` must be first
Even before React. Expo's entry-file hot reload otherwise loses the gesture handler patch and the composer / bottom-sheet gestures silently disable.
```ts
// At the top of App.tsx:
import "react-native-gesture-handler";
// THEN everything else
import React from "react";
```
---
## Step 4 — Env vars
Two options; pick one based on what the project already uses.
### Option A — `app.json extra` + `expo-constants` (simple, recommended)
```json
{
"expo": {
"extra": {
"COMETCHAT_APP_ID": "YOUR_APP_ID",
"COMETCHAT_REGION": "us",
"COMETCHAT_AUTH_KEY": "YOUR_AUTH_KEY"
}
}
}
```
Read via `expo-constants`:
```tsx
import Constants from "expo-constants";
const { COMETCHAT_APP_ID, COMETCHAT_REGION, COMETCHAT_AUTH_KEY } = Constants.expoConfig?.extra ?? {};
```
**Warning**: these values end up in the client bundle. Never put `REST_API_KEY` or any server-side secret in `expo.extra` — use a backend (see `cometchat-native-production`).
### Option B — `.env` + `expo-dotenv` / SDK-native `.env` support
Expo SDK 50+ supports `.env` out of the box via `EXPO_PUBLIC_*` prefix:
```
# .env
EXPO_PUBLIC_COMETCHAT_APP_ID=your_app_id
EXPO_PUBLIC_COMETCHAT_REGION=us
EXPO_PUBLIC_COMETCHAT_AUTH_KEY=your_auth_key
```
Read directly via `process.env.EXPO_PUBLIC_COMETCHAT_APP_ID` anywhere in your app. Any variable WITHOUT the `EXPO_PUBLIC_` prefix is ONLY available in `app.config.js` / server scripts, not bundled — useful for REST API keys in backend-only code.
### Which to choose
- If the project already has `.env` — **Option B**.
- If the project hasn't set up env vars at all — **Option A** (one file, no prefix rules).
- Never mix both for the same variable — pick one place.
---
## Step 5 — Expo Router subsection
Expo Router is a file-based alternative to `@react-navigation/*`. If the project has `app/` instead of (or alongside) `src/screens/`, they're using Expo Router.
Detect Expo Router by checking `package.json` for `expo-router` in dependencies, and `app.json` for the `"expo-router"` plugin.
### 5a — Router entry (`app/_layout.tsx`)
In Expo Router, the `app/_layout.tsx` file is the root layout — wrap the provider chain here instead of in `App.tsx`.
```tsx
// app/_layout.tsx
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
import { CometChatProvider } from "../src/providers/CometChatProvider";
import { Slot } from "expo-router";
import Constants from "expo-constants";
const extra = Constants.expoConfig?.extra ?? {};
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider>
<CometChatProvider
appId={extra.COMETCHAT_APP_ID}
region={extra.COMETCHAT_REGION}
authKey={extra.COMETCHAT_AUTH_KEY}
uid="cometchat-uid-1"
>
<Slot /> {/* Expo Router renders child routes here */}
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
```
### 5b — Conversations + message route
```
app/
_layout.tsx ← provider chain (above)
index.tsx ← home (could be the conversations list)
messages/
[uid].tsx ← dynamic route, one chat per uid
```
```tsx
// app/index.tsx
import { CometChatConversations, CometChatUiKitConstants } from "@cometchat/chat-uikit-react-native";
import { router } from "expo-router";
export default function Home() {
return (
<CometChatConversations
onItemPress={(conversation) => {
const entity = conversation.getConversationWith();
const type = conversation.getConversationType();
if (type === CometChatUiKitConstants.ConversationTypeConstants.user) {
router.push(`/messages/${(entity as any).getUid()}`);
} else {
router.push(`/messages/group-${(entity as any).getGuid()}`);
}
}}
/>
);
}
```
```tsx
// app/messages/[uid].tsx
import { useLocalSearchParams, router } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import {
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react-native";
export default function ChatScreen() {
const { uid } = useLocalSearchParams<{ uid: string }>();
const [user, setUser] = useState<CometChat.User | null>(null);
useEffect(() => {
if (!uid) return;
// Simple UID routing; group routing encodes differently in the index example above
CometChat.getUser(uid).then(setUser).catch(() => setUser(null));
}, [uid]);
if (!user) return null;
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={user} onBack={() => router.back()} showBackButton />
<CometChatMessageList user={user} hideReplyInThreadOption />
<CometChatMessageComposer user={user} />
</View>
);
}
```
### 5c — Tabs in Expo Router (if the project uses them)
```
app/
_layout.tsx
(tabs)/
_layout.tsx ← Tabs layout
chats.tsx
users.tsx
groups.tsx
calls.tsx
messages/
[uid].tsx
```
```tsx
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
export default function TabsLayout() {
return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen name="chats" options={{ title: "Chats" }} />
<Tabs.Screen name="users" options={{ title: "Users" }} />
<Tabs.Screen name="groups" options={{ title: "Groups" }} />
<Tabs.Screen name="calls" options={{ title: "Calls" }} />
</Tabs>
);
}
```
Each tab file renders a single list component (`CometChatConversations`, `CometChatUsers`, etc. — see `cometchat-native-placement` § 2 Bottom tab for the component choices) and pushes to `/messages/[uid]` on press.
### Expo Router gotchas
- **`unstable_settings`** in `_layout.tsx` can break deep linking if set incorrectly. Leave it alone unless you know you need it.
- **Navigation between stack + tabs**: `router.push("/messages/abc")` works from a tab. `router.back()` returns to the tab. No extra configuration needed.
- **Search params**: use `useLocalSearchParams()` (not `useSearchParams()` — that's web-only).
---
## Step 6 — Prebuild + run
Before the first run, Expo needs to generate native projects:
```bash
npx expo prebuild
```
Then run on the platform:
```bash
# iOS (requires Xcode)
npx expo run:ios
# Android (requires Android Studio)
npx expo run:android
# Or EAS for cloud builds
eas build --profile development --platform ios
```
Subsequent runs use `npx expo start` with the dev client — no rebuild needed unless native deps change.
**When to rebuild vs. reload:**
- Changed JS / JSX / TSX → no rebuild, just `r` to reload or save in Fast Refresh
- Added / removed a native dependency → `npx expo prebuild --clean && npx expo run:ios`
- Changed `app.json` permissions or plugins → `npx expo prebuild --clean`
---
## Step 7 — Verify integration
```bash
npx tsc --noEmit # TypeScript check
```
Then in the running app:
1. Open the chat screen you wired
2. Check that the keyboard opens when you tap the composer (gesture handler working)
3. Tap the "+" attachment button — the action sheet should slide up (bottom sheet working)
4. Send a message — it should appear immediately
If any of these don't work, see `cometchat-native-troubleshooting`.
---
## Hard rules
1. **No Expo Go.** The user's project must use development builds. Detect early and tell the user if they're on Expo Go.
2. **`import "react-native-gesture-handler"` is the first line of the entry file** (`App.tsx` or `app/_layout.tsx` for Expo Router). Not second. Not after any React import.
3. **Install all peer deps via `npx expo install`**, not `npm install`, for native modules. Expo's resolver picks SDK-compatible versions.
4. **Never commit `REST_API_KEY`** (or any server-side secret) to `app.json extra` — it ends up in the client bundle. Use a server endpoint (see `cometchat-native-production`).
5. **Merge permissions into `app.json`, don't replace.** The user may already have permissions for other libraries; wipe them out and their other features break.
6. **`npx expo prebuild --clean` after changing `app.json` permissions or adding native deps.** Without it, iOS + Android see the old config.
7. **Every `<CometChatMessageList>` must include `hideReplyInThreadOption`** unless you're also wiring a full thread panel (see `cometchat-native-placement` § Hard rule 5).
8. **The four-wrapper chain goes at the app root**, not per-screen (see `cometchat-native-core` § 3). For Expo Router, that's `app/_layout.tsx`. For plain Expo, that's `App.tsx`.
---
## Common questions
**Q: Can I use `npx create-expo-app --template tabs`?**
Yes — the tabs template already has Expo Router set up. Just add the provider chain in `app/_layout.tsx` per § 5a and replace a tab's content with a CometChat component.
**Q: Can I use SDK ≤49?**
Expo SDK 49 may work but the CometChat peer deps target 50+ conventions. If the user is stuck on an older SDK, ask them to upgrade — or fall back to bare RN via `npx expo prebuild` + `cometchat-native-bare-patterns`.
**Q: I'm seeing "Main module field cannot be resolved" when opening Expo Go.**
That's the "Expo Go doesn't support native modules" error. Build a dev client: `eas build --profile development` or `npx expo run:ios`.
**Q: My app crashes on first push-notification receive.**
Push notifications need additional setup (APNs + FCM + maybe `expo-notifications`). Out of scope for this skill — see `cometchat-native-troubleshooting` § Push notifications.
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Init / login / wrapper chain / anti-patterns |
| `cometchat-native-components` | Component prop reference |
| `cometchat-native-placement` | Where chat goes (stack / tabs / modal / bottom sheet / embedded) |
| `cometchat-native-expo-patterns` | This skill — Expo managed workflow specifics |
| `cometchat-native-bare-patterns` | Bare RN (pod install, native modules, privacy manifest) |
| `cometchat-native-features` | Calls, extensions, AI |
| `cometchat-native-theming` | Theme customization |
| `cometchat-native-customization` | Text formatters, events, custom views |
| `cometchat-native-production` | Server-side auth tokens + user management |
| `cometchat-native-troubleshooting` | Prebuild failures, Expo Go errors, keyboard issues, blank chat |
---
name: cometchat-native-features
description: "Feature catalog for React Native — calls (separate SDK + WebRTC), extensions (polls / stickers / translation / link preview / collaborative doc / whiteboard / smart replies), AI agent, in-call chat. When to toggle, install, or swap."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native features calls extensions ai"
---
## Purpose
Teaches Claude how to add features on top of a working CometChat React Native integration. Classifies each feature into one of four types and gives the correct recipe for each.
**Read `cometchat-native-core` + `cometchat-native-components` + (`cometchat-native-expo-patterns` or `cometchat-native-bare-patterns`) first** — a base integration must already exist before features layer on.
Ground truth: `docs/ui-kit/react-native/core-features.mdx`, `calling-integration.mdx`, `call-*.mdx`, `incoming-call.mdx`, `outgoing-call.mdx`, `extensions.mdx`, `guide-ai-agent.mdx`, `ai-assistant-chat-history.mdx`, and `@cometchat/chat-uikit-react-native@5.3.3` exports.
---
## 1. Feature taxonomy
Every CometChat feature falls into exactly one of four categories. The category determines the recipe:
| Category | What it means | Example features | How to enable |
|---|---|---|---|
| **Default** | Already on — no action needed. Shipped with the kit's base components. | Instant messaging, typing indicators, read receipts, reactions on messages, replies, @mentions, media upload, edit/delete, message info | Just render `CometChatMessageHeader` + `CometChatMessageList` + `CometChatMessageComposer` |
| **Dashboard-toggle** | Flip an extension toggle in the CometChat dashboard (or via the CLI). UI Kit auto-wires the feature once enabled. | Polls, stickers, smart replies, message translation, link previews, collaborative whiteboard, collaborative document, thumbnail generation | Toggle on → hard-reload the app |
| **Package-install** | Install an additional npm package + maybe native peer deps. The UI Kit auto-detects the package on next init. | Voice + video calls (`@cometchat/calls-sdk-react-native`) | `npm install ...` → pod install (iOS) → rebuild |
| **Component-swap** | Replace or wrap a UI Kit component with a customized version. | Custom text formatter (emoji shortcuts, custom tags), custom message templates, AI Agent chat history | Write a new component + pass via prop |
Reminder — don't confuse these with **customization** (per-skill-coverage under `cometchat-native-customization`). This skill is "add a feature that CometChat ships"; customization is "change how an existing feature looks or behaves".
---
## 2. Enabling dashboard-toggle features
Most extensions (polls, stickers, translation, link preview, smart replies, collaborative doc, collaborative whiteboard, thumbnails) are **dashboard-toggle** features. To enable one:
### Option A — use the CLI (preferred — no dashboard trip)
```bash
npx @cometchat/skills-cli features list --json # browse available features
npx @cometchat/skills-cli features info polls --json # details for one
npx @cometchat/skills-cli features enable polls --json # flip the toggle
```
The CLI reads the app ID from `.cometchat/config.json` and the bearer token from the OS keychain (requires a prior `cometchat auth login`). Response:
- `"status": "enabled"` → done. Hard-reload (stop Metro + restart + rebuild if on iOS).
- `"status": "no-op"` → already enabled.
- `"status": "not-logged-in"` → `cometchat auth login` first.
- `"status": "no-app"` → run `/cometchat` or `cometchat provision setup` first.
- `"status": "error"` → surface `next_steps` — includes the dashboard URL as a manual fallback.
**Only fall back to the dashboard walkthrough if the CLI errors.**
### Option B — dashboard (fallback when CLI isn't available)
1. https://app.cometchat.com → your app
2. Chat & Messaging → Features
3. Find the extension by name → flip Status ON
4. Hard-reload the RN app (stop Metro, restart, rebuild native if the extension adds native deps — most don't)
### What each toggle does
| Extension | UI surface when enabled |
|---|---|
| Polls | Polls option in `CometChatMessageComposer`'s attachment Action Sheet |
| Stickers | Sticker picker in the composer |
| Smart replies | Chip suggestions above the composer input after an incoming message |
| Message translation | "Translate" option in the message long-press menu |
| Link preview | Rich-card bubble for URLs in the message list |
| Collaborative document | Option in composer's Action Sheet; opens a shared doc on tap |
| Collaborative whiteboard | Option in composer's Action Sheet; opens a shared canvas |
| Thumbnail generation | Image / video bubbles show thumbnails instead of full-size downloads |
### Gotcha — `auto_wired_in_uikit: false`
A minority of extensions need extra wiring via `UIKitSettingsBuilder.setExtensions([...])` before `init`. The CLI flags this in its success response:
```json
{
"status": "enabled",
"name": "stickers",
"auto_wired_in_uikit": false,
"next_steps": [
"Register the extension in UIKitSettingsBuilder.setExtensions([...]) before CometChatUIKit.init()"
]
}
```
If `auto_wired_in_uikit` is `false`, import the matching `ExtensionsDataSource` from `@cometchat/chat-uikit-react-native` and pass it to the builder:
```tsx
import {
UIKitSettingsBuilder,
CometChatUIKit,
StickersExtension,
PollsExtension,
} from "@cometchat/chat-uikit-react-native";
const settings = new UIKitSettingsBuilder()
.setAppId(APP_ID)
.setRegion(REGION)
.setAuthKey(AUTH_KEY)
.setExtensions([new StickersExtension(), new PollsExtension()]) // ← new
.subscribePresenceForAllUsers()
.build();
await CometChatUIKit.init(settings);
```
Query the docs MCP for the exact extension class name if you don't remember it — `extensions.mdx` lists all of them.
---
## 3. Calls (package-install)
Calls are the biggest "add a feature" step. They require the separate `@cometchat/calls-sdk-react-native` package, additional peer native modules (WebRTC + netinfo + background-timer + callstats), and app-side listener setup.
### 3a — Install the calls SDK + peer deps
**Expo (managed workflow)**:
```bash
npm install @cometchat/calls-sdk-react-native
npx expo install \
@react-native-community/netinfo \
react-native-background-timer \
react-native-callstats \
react-native-webrtc
npx expo prebuild --clean
```
**Bare RN**:
```bash
npm install \
@cometchat/calls-sdk-react-native \
@react-native-community/netinfo \
react-native-background-timer \
react-native-callstats \
react-native-webrtc
cd ios && pod install && cd ..
```
### 3b — Platform permissions (if not already from core integration)
**iOS** — `ios/<App>/Info.plist`:
```xml
<key>NSCameraUsageDescription</key>
<string>Camera access for video calls</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access for voice and video calls</string>
```
**Android** — `android/app/src/main/AndroidManifest.xml`:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```
### 3c — iOS deployment target + build settings
Calls SDK requires iOS 12+ and specific Podfile flags. Add to `ios/Podfile`:
```ruby
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386'
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
```
Android (`android/app/build.gradle`):
```groovy
android {
compileSdkVersion 33
defaultConfig {
minSdkVersion 24
targetSdkVersion 33
}
}
```
### 3d — Register the call listener at app root
The incoming-call UI only shows up if you've registered a listener to pick up call events. Add this once at the app root (typically in `App.tsx` or Expo Router's `_layout.tsx`):
```tsx
import React, { useEffect, useRef, useState } from "react";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { CometChatIncomingCall } from "@cometchat/chat-uikit-react-native";
function CallEventsProvider({ children }: { children: React.ReactNode }) {
const [callReceived, setCallReceived] = useState(false);
const incomingCall = useRef<CometChat.Call | null>(null);
const LISTENER_ID = "APP_CALL_LISTENER";
useEffect(() => {
CometChat.addCallListener(
LISTENER_ID,
new CometChat.CallListener({
onIncomingCallReceived: (call) => {
incomingCall.current = call;
setCallReceived(true);
},
onOutgoingCallAccepted: (call) => {
// navigate to the ongoing-call screen
},
onOutgoingCallRejected: () => {
incomingCall.current = null;
setCallReceived(false);
},
onIncomingCallCancelled: () => {
incomingCall.current = null;
setCallReceived(false);
},
onCallEndedMessageReceived: () => {
incomingCall.current = null;
setCallReceived(false);
},
})
);
return () => CometChat.removeCallListener(LISTENER_ID);
}, []);
return (
<>
{children}
{callReceived && incomingCall.current && (
<CometChatIncomingCall
call={incomingCall.current}
onAccept={() => { /* navigate to ongoing-call screen */ }}
onDecline={() => setCallReceived(false)}
/>
)}
</>
);
}
```
Wrap the app (inside the existing provider chain, below `CometChatProvider`):
```tsx
<CometChatProvider ...>
<CallEventsProvider>
<AppNavigator />
</CallEventsProvider>
</CometChatProvider>
```
### 3e — Call buttons in the message header
Once the calls SDK is installed, `CometChatMessageHeader` auto-renders voice + video call buttons. To customize (e.g., hide one):
```tsx
<CometChatMessageHeader
user={selectedUser}
hideVoiceCallButton={false}
hideVideoCallButton={false}
AuxiliaryButtonView={(user, group) => (
<CometChatCallButtons
user={user}
group={group}
onVoiceCallPress={(session) => navigation.navigate("OngoingCall", { session })}
onVideoCallPress={(session) => navigation.navigate("OngoingCall", { session })}
/>
)}
/>
```
### 3f — Ongoing call screen
Navigate to a dedicated screen that hosts `CometChatOngoingCall` when a call connects:
```tsx
// OngoingCallScreen.tsx
import { CometChatOngoingCall } from "@cometchat/chat-uikit-react-native";
export function OngoingCallScreen({ route, navigation }: any) {
const { session } = route.params;
return (
<CometChatOngoingCall
sessionID={session.sessionId}
callType={session.type} // "audio" | "video"
onCallEnded={() => navigation.goBack()}
/>
);
}
```
### 3g — Call logs
A history view of past calls. Typically one tab in a tab-based layout (see `cometchat-native-placement` § 2):
```tsx
import { CometChatCallLogs } from "@cometchat/chat-uikit-react-native";
export function CallLogsScreen() {
return <CometChatCallLogs onItemPress={(callLog) => openCallDetails(callLog)} />;
}
```
### 3h — Verifying calls work
1. Rebuild the app after adding the calls SDK (Expo: `expo run:ios` / `run:android`; bare: `npx react-native run-ios` / `run-android`)
2. Log in as one user on device A, another on device B
3. On device A, tap the voice or video call icon in the message header
4. Device B should show `CometChatIncomingCall` within a few seconds
5. Accept on B → both devices transition to `CometChatOngoingCall`
If incoming calls don't show: listener not registered, or the listener ID collides. See `cometchat-native-troubleshooting`.
### 3i — Testing calls in CI
Real WebRTC calls can't run in Jest or Maestro — they need two real
devices plus a TURN server. What you CAN test:
- **Call button renders** — mount `CometChatMessageHeader` with a user
prop and assert the call `testID` is present (requires the calls SDK
import to not crash; mock `@cometchat/calls-sdk-react-native` in
your jest setup).
- **Call listener registration** — spy on `CometChat.addCallListener`
from the SDK mock and assert your `registerCallListener()` fires
exactly once per mount.
- **Incoming-call UI** — render `<CometChatIncomingCall call={mockCall} />`
with a stubbed `CometChat.Call` object; assert accept/reject buttons
are wired.
See `cometchat-native-testing § 5` for the SDK mock shape (includes a
`addCallListener` / `removeCallListener` stub) and § 10 for why actual
call E2E belongs in manual QA, not Detox/Maestro.
---
## 4. In-call chat (optional, during-call feature)
During an active call, users can chat without leaving the call UI. This is a toggle on the ongoing-call component:
```tsx
<CometChatOngoingCall
sessionID={session.sessionId}
callType={session.type}
callSettingsBuilder={/* ... with enableInCallChat(true) */}
onCallEnded={() => navigation.goBack()}
/>
```
In-call chat adds a collapsible chat panel to the call screen. Participants see messages for the duration of the call.
---
## 5. AI Agent (component-swap + dashboard)
The AI Agent integration adds an AI-powered conversational assistant to your app. Two parts:
### 5a — Dashboard setup
1. https://app.cometchat.com → your app → AI → Agents
2. Create a new agent (name, system prompt, model)
3. Assign a UID to the agent (e.g. `ai-support-agent`)
Once the agent exists in the dashboard, users can message it like any other user.
### 5b — Optional: AI Assistant Chat History UI
The UI Kit exports `CometChatAIAssistantChatHistory` for apps that want a dedicated AI-chat entry point (distinct from a regular chat with a human user). This component shows past AI conversations and a "New chat" trigger.
```tsx
import { CometChatAIAssistantChatHistory } from "@cometchat/chat-uikit-react-native";
export function AIChatScreen({ navigation }: any) {
return (
<CometChatAIAssistantChatHistory
user={loggedInUser}
onMessageClicked={(message) => navigation.navigate("AIChat", { message })}
onNewChatButtonClick={() => navigation.navigate("AIChat", { new: true })}
/>
);
}
```
The actual AI-chat screen is a regular `CometChatMessageHeader` + `MessageList` + `Composer` composition targeted at the AI agent's UID.
### 5c — AI features beyond the basic agent
See `ai-assistant-chat-history.mdx` and `guide-ai-agent.mdx` in the docs for:
- Multi-tool agents (tools registered via `setAIAssistantTools()`)
- Streaming responses (handled automatically via `CometChatAIAssistantMessageBubble`)
- Agent memory and persona
These are advanced topics — query the docs MCP for the current API if the user wants any of them.
---
## 6. Core features (no new code, no new install)
The following are shipped by default in every CometChat integration — they work from day 1 without any feature-enabling step. Mention them to users who ask "what do I get out of the box?":
- **Instant messaging** (text, with real-time delivery)
- **Media sharing** (images, video, audio, files)
- **Read receipts** (single tick = sent, double tick = delivered, blue = read)
- **Typing indicators**
- **@mentions** (requires `CometChatMentionsFormatter` in `textFormatters`, already in default config)
- **Reactions** (long-press any message to add emoji reaction)
- **Replies** (swipe or long-press → Reply)
- **Edit / delete** own messages
- **Message info** — sender sees delivery + read timestamps per-recipient
- **Mark as unread**
- **Voice messages** (record + send from composer)
- **Search** (`CometChatSearch` component — scoped or global)
- **Group management** (create, add members, leave, mute, transfer ownership)
If a user reports "X isn't working" for a core feature, it's likely a props issue (e.g., `hideReceipts={true}` accidentally set) or a dashboard setting, not a missing feature.
---
## 7. Finding a feature's category quickly
When a user asks for a feature, use this flow:
1. **Is it in the core-features list (§ 6)?** → Already works. Confirm no `hide*` prop is turning it off.
2. **Is it voice / video / call history?** → Calls (§ 3). Package install.
3. **Is it polls, stickers, smart replies, translation, link previews, collaborative doc / whiteboard, thumbnails?** → Extension (§ 2). Dashboard toggle.
4. **Is it AI agent?** → § 5.
5. **Is it custom text formatting, custom message templates, custom slot views, custom theme?** → This is **customization**, not a feature. Route to `cometchat-native-customization` or `cometchat-native-theming`.
6. **Not on this list?** → Check `docs/ui-kit/react-native/guide-overview.mdx` + the kit's `src/index.ts` exports. If still nothing, tell the user the feature isn't in the UI Kit; they may need to build it with the SDK directly.
---
## 8. Anti-patterns
1. **Do NOT speculatively install the calls SDK.** WebRTC bloats the app binary by several MB. Install only after the user says they want calls.
2. **Do NOT enable extensions your app doesn't use.** Each enabled extension adds data-fetching overhead and surface area for UI glitches. Turn on what the user asks for.
3. **Do NOT modify UI Kit source to add a feature.** If the feature isn't in the default + extension + calls list, build on top (custom template, custom slot view) — don't fork the kit.
4. **Do NOT wire the call listener per-screen.** It should be once, at the app root. Per-screen registration causes missed incoming calls when the user isn't on the registering screen.
5. **Do NOT skip the `cometchat features enable` step if the CLI is available.** Telling the user to visit the dashboard when the CLI could have flipped the toggle for them is worse UX.
6. **Do NOT forget `pod install` + rebuild after installing calls SDK.** The WebRTC native module won't load otherwise.
7. **Do NOT reference AI tools, streaming, or agent memory from memory.** These APIs change across UIKit minor versions. Query the docs MCP before generating code.
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Init / login / provider chain |
| `cometchat-native-components` | Component prop reference (for `CometChatCallButtons`, `CometChatOngoingCall`, etc.) |
| `cometchat-native-placement` | Where the ongoing-call screen, call-logs tab, AI chat screen go |
| `cometchat-native-expo-patterns` | Expo-specific calls SDK setup (`expo prebuild`) |
| `cometchat-native-bare-patterns` | Bare RN calls SDK setup (Podfile flags, deployment target) |
| `cometchat-native-features` | This skill — which features exist + how to enable each |
| `cometchat-native-theming` | Theming call buttons, reaction colors, extension UI colors |
| `cometchat-native-customization` | Custom text formatters, custom message templates, event bus |
| `cometchat-native-push` | Push for calls uses a separate VoIP channel (`APNS_REACT_NATIVE_VOIP`) — see push § 7 |
| `cometchat-native-testing` | Mocking the calls SDK + incoming-call UI tests — see § 3i |
| `cometchat-native-production` | Production auth tokens (no feature concern, but the prerequisite for AI agent in prod) |
| `cometchat-native-troubleshooting` | Incoming call not ringing, extension not showing after enabling, call permissions denied |
---
name: cometchat-native-placement
description: "Where to put chat in a React Native app — Stack screen, BottomTab, Modal, BottomSheet, Embedded. Maps each to CometChat component composition with ASCII layout references."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native placement stack tabs modal bottomsheet embedded"
---
## Purpose
Teaches Claude the five canonical placement patterns for putting chat inside a React Native app. Each pattern specifies:
1. Which CometChat components to compose
2. How to wire the placement into `@react-navigation/*` (or Expo Router)
3. Platform gotchas (safe-area, keyboard avoiding, gesture handling)
4. When to choose this placement over the alternatives
Ground truth: `docs/ui-kit/react-native/react-native-conversation.mdx`, `react-native-one-to-one-chat.mdx`, `react-native-tab-based-chat.mdx`, their `expo-*.mdx` equivalents, and the `examples/SampleApp/` + `examples/SampleAppExpo/` sample apps.
**Read `cometchat-native-core` and `cometchat-native-components` before this skill** — the provider wrapper chain and component catalog are prerequisites.
---
## "What are you building?" — placement recommendation
Use this table to pick a placement. If the user says "add chat to my app" without specifying where, ask them what they're building.
| User intent | Recommended placement | Experience |
|---|---|---|
| Messaging app (WhatsApp / Telegram / Signal style) | **Conversations stack** — list → tap → full-page messages screen | Two-pane-equivalent on mobile |
| SaaS / marketplace / e-commerce with chat as a feature | **Stack screen** — dedicated `/chat` or `/messages` route | Full-page chat inside the app |
| Support app or focused 1-to-1 | **Stack screen (single thread)** — no conversation list, go straight into one chat | Single thread |
| Full messaging hub with calls / users / groups | **Bottom tabs** — Chats / Users / Groups / Calls tabs + stack screen for message view | Tab-based messenger |
| Occasional chat overlay from a non-chat screen | **Modal** — present from anywhere, dismiss to return | Modal |
| Inline comments / contextual chat | **BottomSheet** — swipe up from a screen section | Sheet |
| Chat embedded inside an existing screen (e.g. a support tab next to product details) | **Embedded** — CometChat components inside a parent layout | Embedded |
---
## Visual reference — five RN placement patterns
### 1. Stack screen (full page)
```
┌───────────────────────────────────┐
│ ← Hiking Group ⋮ │ ← CometChatMessageHeader
├───────────────────────────────────┤
│ │
│ ╭──────────╮ │
│ │ Message │ │
│ ╰──────────╯ │ ← CometChatMessageList
│ │
│ ╭──────────╮ │
│ │ Reply │ │
│ ╰──────────╯ │
│ │
├───────────────────────────────────┤
│ + Type a message... ▶ │ ← CometChatMessageComposer
└───────────────────────────────────┘
```
### 2. Bottom tab
```
┌───────────────────────────────────┐
│ ← Hiking Group ⋮ │ ← header
├───────────────────────────────────┤
│ │
│ (messages) │
│ │
├───────────────────────────────────┤
│ Chats Users Groups Calls │ ← bottom tab bar
└───────────────────────────────────┘
```
### 3. Modal (slide-up over current screen)
```
┌─────────────────┐
│ ═══ Chat ✕ │ ← drag handle + close
├─────────────────┤
│ │
│ (messages) │
│ │
├─────────────────┤
│ Type message ▶ │
└─────────────────┘
(parent screen dimmed behind)
```
### 4. BottomSheet (swipe-up partial)
```
parent screen visible at top ─────
┌─────────────────┐
│ ═══ (handle) │
│ Hiking Group │
├─────────────────┤
│ (messages) │
├─────────────────┤
│ Type message ▶ │
└─────────────────┘
```
### 5. Embedded (inside an existing screen)
```
┌───────────────────────────────────┐
│ Product details │
│ [product image + specs] │
├───────────────────────────────────┤
│ Contact seller │ ← section heading
│ ┌────────────────────────────┐ │
│ │ (CometChatMessageHeader) │ │
│ │ (CometChatMessageList) │ │ ← embedded chat
│ │ (CometChatMessageComposer) │ │
│ └────────────────────────────┘ │
└───────────────────────────────────┘
```
---
## 1. Stack screen
The most common pattern — chat lives in its own screen, pushed via `@react-navigation/native-stack`.
### Pattern A — Conversations list → Messages
Two screens: list + messages.
```tsx
// ConversationsScreen.tsx
import { CometChatConversations, CometChatUiKitConstants } from "@cometchat/chat-uikit-react-native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
export function ConversationsScreen({ navigation }: { navigation: NativeStackNavigationProp<any> }) {
return (
<CometChatConversations
onItemPress={(conversation) => {
const type = conversation.getConversationType();
if (type === CometChatUiKitConstants.ConversationTypeConstants.user) {
navigation.navigate("Messages", { user: conversation.getConversationWith() });
} else {
navigation.navigate("Messages", { group: conversation.getConversationWith() });
}
}}
/>
);
}
```
```tsx
// MessagesScreen.tsx
import { View } from "react-native";
import {
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react-native";
export function MessagesScreen({ route, navigation }: any) {
const { user, group } = route.params ?? {};
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={user} group={group} onBack={() => navigation.goBack()} showBackButton />
<CometChatMessageList user={user} group={group} hideReplyInThreadOption />
<CometChatMessageComposer user={user} group={group} />
</View>
);
}
```
```tsx
// AppNavigator.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Conversations" component={ConversationsScreen} />
<Stack.Screen name="Messages" component={MessagesScreen} />
</Stack.Navigator>
```
### Pattern B — Single thread (no conversation list)
For support chat, marketplace "Contact seller", or any focused 1-to-1 where the target user/group is known in advance.
```tsx
export function SupportChatScreen() {
const [agent, setAgent] = useState<CometChat.User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
CometChat.getUser("support-agent-uid")
.then((user) => {
setAgent(user);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
if (loading) return <ActivityIndicator style={{ flex: 1 }} />;
if (!agent) return <Text style={{ padding: 16 }}>Support unavailable. Try again shortly.</Text>;
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
);
}
```
### Navigation wiring notes
- The screen is wrapped in a `<View style={{ flex: 1 }}>` so the composer sits at the bottom and the list fills the middle.
- `CometChatMessageHeader`'s `onBack` should call `navigation.goBack()`. Set `showBackButton` explicitly so the header knows to render it.
- **Keyboard avoiding**: when the composer is visible, RN needs `KeyboardAvoidingView` on iOS or `android:windowSoftInputMode="adjustResize"` on Android. The framework patterns (`cometchat-native-expo-patterns`, `cometchat-native-bare-patterns`) cover the platform-specific wiring.
---
## 2. Bottom tab
For full-featured messengers with distinct entry points per content type.
```tsx
// TabsNavigator.tsx
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
function MainTabs() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Chats" component={ConversationsScreen} />
<Tab.Screen name="Users" component={UsersScreen} />
<Tab.Screen name="Groups" component={GroupsScreen} />
<Tab.Screen name="Calls" component={CallLogsScreen} />
</Tab.Navigator>
);
}
export function AppNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Main" component={MainTabs} />
<Stack.Screen name="Messages" component={MessagesScreen} />
</Stack.Navigator>
);
}
```
Each tab screen pushes to a shared `Messages` stack screen with the selected entity:
```tsx
export function UsersScreen({ navigation }: any) {
return (
<CometChatUsers onItemPress={(user) => navigation.navigate("Messages", { user })} />
);
}
export function GroupsScreen({ navigation }: any) {
return (
<CometChatGroups onItemPress={(group) => navigation.navigate("Messages", { group })} />
);
}
export function CallLogsScreen() {
return <CometChatCallLogs />;
}
```
### Wiring notes
- Tabs use `@react-navigation/bottom-tabs`. The `Messages` screen is OUTSIDE the tab navigator (at the stack level) so it presents full-screen without the tab bar.
- For the **Calls** tab, `CometChatCallLogs` only works when `@cometchat/calls-sdk-react-native` is installed. Omit the Calls tab if the project doesn't use calling.
---
## 3. Modal
For occasional chat that doesn't belong in the primary navigation. Two approaches — native RN `<Modal>` or react-navigation's `presentation: "modal"`.
### Pattern A — React Navigation modal (recommended)
Cleaner — the modal is a regular stack screen with a modal presentation option.
```tsx
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="ChatModal"
component={ChatModalScreen}
options={{ presentation: "modal" }}
/>
</Stack.Navigator>
```
```tsx
function ChatModalScreen({ navigation }: any) {
const [agent, setAgent] = useState<CometChat.User | null>(null);
useEffect(() => { CometChat.getUser("support-agent").then(setAgent); }, []);
if (!agent) return null;
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={agent} onBack={() => navigation.goBack()} showBackButton />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
);
}
// Trigger from anywhere:
<Button title="Contact support" onPress={() => navigation.navigate("ChatModal")} />
```
iOS gets the native modal slide-up. Android shows a fade-in full-screen by default — if you need a swipe-to-dismiss feel, use the BottomSheet pattern instead.
### Pattern B — RN `<Modal>` component
For lightweight one-off modals that don't need a separate route.
```tsx
import { Modal, Pressable, View } from "react-native";
const [visible, setVisible] = useState(false);
<Modal visible={visible} animationType="slide" onRequestClose={() => setVisible(false)}>
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={agent} onBack={() => setVisible(false)} showBackButton />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
</SafeAreaView>
</Modal>
```
Works fine but bypasses navigation state — deep links and back-button handling need extra work.
---
## 4. BottomSheet
Native-feel swipe-up chat overlaid on a parent screen. Two library options; pick one based on the project's existing navigation:
| Library | When to use |
|---|---|
| `@gorhom/bottom-sheet` | Most flexible + most common. Good for partial-height sheets with snap points. |
| `@cometchat/chat-uikit-react-native`'s `CometChatBottomSheet` | Lightweight. Good if the project doesn't already depend on `@gorhom/bottom-sheet`. |
### Pattern A — @gorhom/bottom-sheet
```tsx
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
import { useRef, useMemo } from "react";
function ProductScreen({ product }: any) {
const sheetRef = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ["25%", "90%"], []);
const [agent, setAgent] = useState<CometChat.User | null>(null);
useEffect(() => {
CometChat.getUser(product.sellerUid).then(setAgent);
}, [product.sellerUid]);
return (
<View style={{ flex: 1 }}>
<ProductDetails product={product} />
<Button title="Contact seller" onPress={() => sheetRef.current?.expand()} />
<BottomSheet ref={sheetRef} snapPoints={snapPoints} index={-1} enablePanDownToClose>
<BottomSheetView style={{ flex: 1 }}>
{agent && (
<>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</>
)}
</BottomSheetView>
</BottomSheet>
</View>
);
}
```
### Pattern B — CometChatBottomSheet
```tsx
import { CometChatBottomSheet } from "@cometchat/chat-uikit-react-native";
const sheetRef = useRef<any>(null);
<CometChatBottomSheet ref={sheetRef}>
<View style={{ flex: 1, height: "100%" }}>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
</CometChatBottomSheet>
<Button title="Chat" onPress={() => sheetRef.current?.show()} />
```
### BottomSheet gotchas
- **Keyboard behavior**: `@gorhom/bottom-sheet` has `keyboardBehavior` + `keyboardBlurBehavior` props. Without them the composer gets covered by the keyboard on iOS. Use `keyboardBehavior="interactive"` + `keyboardBlurBehavior="restore"`.
- **Gesture handler wrap**: BottomSheet requires `<GestureHandlerRootView style={{ flex: 1 }}>` at the root (already required by the UI Kit — see `cometchat-native-core` § 3).
- **Height**: Pass `flex: 1` + `height: "100%"` on the inner View so the message list expands to fill the sheet.
---
## 5. Embedded
Chat inside an existing screen, not its own route.
```tsx
export function ProductDetailScreen({ product }: any) {
const [agent, setAgent] = useState<CometChat.User | null>(null);
useEffect(() => { CometChat.getUser(product.sellerUid).then(setAgent); }, [product.sellerUid]);
return (
<ScrollView style={{ flex: 1 }} keyboardShouldPersistTaps="handled">
<ProductImages images={product.images} />
<ProductSpecs product={product} />
<View style={{ marginTop: 24 }}>
<Text style={{ fontSize: 18, fontWeight: "600", padding: 16 }}>Chat with seller</Text>
<View style={{ height: 480 }}>
{agent && (
<>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</>
)}
</View>
</View>
</ScrollView>
);
}
```
### Embedded gotchas
- **Fixed height required.** CometChat components fill 100% of their parent. If you put them inside a `ScrollView` without a bounded height, the list collapses to zero height. Wrap in a `<View style={{ height: NNN }}>` or flex container with an explicit height.
- **Scroll conflict.** If the parent is a `ScrollView`, the message list's internal scroll competes with the parent's scroll. Consider the single-thread-as-stack-screen pattern instead if the chat is a primary UX.
- **Composer focus.** When the user taps the composer, the keyboard rises and can push the embedded chat off-screen on iOS. `keyboardShouldPersistTaps="handled"` on the parent ScrollView + `KeyboardAvoidingView` at the root help.
Usually the embedded pattern is the wrong default — prefer a Modal or BottomSheet trigger from a button on the screen, which gives users a dedicated surface for chatting.
---
## Hard rules
These apply to ALL placement patterns. Violating any of them causes integration bugs or destroys the existing navigation.
1. **NEVER modify the project's existing navigator without reading it first.** Understand what's there before adding screens or tabs. Don't replace a user's navigation structure unless they explicitly chose "demo mode."
2. **ALWAYS use a separate screen / stack entry for chat**, not inline replacement of an existing screen. The one exception is embedded placement (§ 5) where chat is explicitly part of a bigger screen.
3. **The four-wrapper chain is required at the app root**, not per-screen (see `cometchat-native-core` § 3). Re-wrapping per screen causes duplicate init + login, dropped WebSockets, and a 2–3-second flicker on first mount.
4. **`import "react-native-gesture-handler"`** must be at the very top of `index.js` (or Expo entry). Missing this import silently disables swipe gestures in the composer, bottom sheet, and attachment drawer.
5. **Every `<CometChatMessageList>` MUST include `hideReplyInThreadOption`** unless the integration also wires a full thread panel (`CometChatThreadHeader` + scoped list + scoped composer with `parentMessageId`). Drawer / modal / bottom sheet / embedded / stack-screen placements without a thread panel **must include the flag** — otherwise "Reply in Thread" shows in the message menu and silently does nothing.
6. **Resolve user / group before rendering.** The component props `user` and `group` expect `CometChat.User` and `CometChat.Group` instances — not bare UID strings. Fetch via `CometChat.getUser(uid)` / `CometChat.getGroup(guid)` in a `useEffect` and gate the render on the resolved object.
7. **Pass either `user` or `group`, never both.** Passing both causes runtime errors. Branch in render based on which one is set.
8. **Every CometChat container must have explicit flex height.** Components fill 100% of parent. If parent has no bounded height (`flex: 1`, `height: N`, or inside a flex layout with `flex: N`), components collapse to zero height and render empty. This is THE most common "why is my chat blank" bug.
9. **For modals and bottom sheets, set `keyboardShouldPersistTaps="handled"`** on any ScrollView / FlatList parent and configure keyboard behavior explicitly. Otherwise the composer gets hidden by the keyboard on iOS.
10. **Never animate a CometChat-containing container with `transform`** (including Tailwind's `translate-x-*` / `translate-y-*` / `scale-*` / `rotate-*` utilities if using NativeWind). `transform` creates a new containing block for `position: "absolute"` descendants, which reparents CometChat's absolute-positioned overlays (emoji picker, action sheet, reactions popover) and makes them misalign. In RN this is less common than web (RN has no `position: fixed`) but the same rule applies to any `position: absolute` pickers. Animate `right` / `left` / `top` / `bottom` offsets instead.
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Always first — init, login, provider wrapper chain |
| `cometchat-native-components` | For component prop details — always |
| `cometchat-native-placement` | This skill — picking + wiring a placement |
| `cometchat-native-expo-patterns` | Expo-specific integration (app.json, permissions, Expo Router) |
| `cometchat-native-bare-patterns` | Bare RN (pod install, native modules, privacy manifest) |
| `cometchat-native-theming` | Customize colors / typography / dark mode |
| `cometchat-native-features` | Calls, extensions, AI — the "add a feature" flow |
| `cometchat-native-customization` | Custom slot views, text formatters, events |
| `cometchat-native-production` | Server-side auth tokens |
| `cometchat-native-troubleshooting` | Blank chat / gestures not working / keyboard covering composer / pod install fails |
---
name: cometchat-native-production
description: "Production-readiness for React Native — server-minted auth tokens, user management CRUD, external-backend recipes (Express / Hono / Firebase Functions / Vercel Serverless). RN has no API routes, so the backend is always external."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native production auth token security user-management rest-api"
---
## Purpose
Teaches Claude how to move a React Native CometChat integration from dev-mode Auth Key to production-ready server-minted auth tokens + user CRUD. Covers:
1. Why the dev `authKey` can't ship to production
2. Auth Key vs REST API Key — which lives where
3. Server endpoint recipes (Express / Hono / Firebase Functions / Vercel)
4. Client-side: `CometChatUIKit.login({ authToken })` + token refresh
5. User CRUD endpoints + auth-provider integration (Firebase Auth / Supabase / Clerk / Auth0)
6. Security checklist + rate limits
**Read `cometchat-native-core` first** (init/login/wrapper chain) before this skill — production just swaps one prop on the provider, but understanding the provider lifecycle is the prerequisite.
Ground truth: `docs/ui-kit/react-native/methods.mdx`, and the cross-platform REST API at `https://{APP_ID}.api-{REGION}.cometchat.io/v3/`.
---
## 1. Why production auth matters
In dev mode, `CometChatUIKit.login({ uid: "..." })` uses the `authKey` configured via `UIKitSettingsBuilder.setAuthKey()`. That key is embedded in your React Native bundle. For a signed iOS `.ipa` or Android `.apk`/`.aab`, anyone can extract it with standard reverse-engineering tools (unzip, strings, `apktool`, `ReverseAPK`) and use it to log in as **ANY** user in your CometChat app — read private messages, send as other users, access every conversation.
Production MUST use server-side token generation:
- Your **server** holds the REST API Key (a different key from the client Auth Key).
- On user login, your server calls CometChat's REST API with the REST API Key to mint a short-lived **Auth Token** for that specific UID.
- Your client receives the Auth Token and calls `CometChatUIKit.login({ authToken })`.
- If the token leaks, the blast radius is one user session, not your whole app.
**Exactly the same threat model as JWTs for a REST API.** If you've built a login flow before, this is that.
---
## 2. Auth Key vs REST API Key — two different keys
Easy to confuse. Both come from the CometChat Dashboard (your app → API & Auth Keys), but they live in different places and have different privileges.
| Key | Where in dashboard | Purpose | Where it lives |
|---|---|---|---|
| **Auth Key** | "Auth Keys" table | Client-side SDK `login({ uid })` in dev mode | **Client bundle** — dev only. Never in production builds. |
| **REST API Key** | "REST API Keys" table | Server-to-server: token generation, user CRUD, custom-message-send | **Server only.** Never in an RN bundle, `app.json extra`, `EXPO_PUBLIC_*` var, or git-committed file. |
If the project only has an Auth Key, the user needs to generate a REST API Key in the dashboard: **API & Auth Keys → REST API Keys → Add Key**. Pick "Full Access" for server-side use.
---
## 3. The token auth pattern (4 steps)
```
1. Client logs into YOUR auth (Firebase Auth / Supabase / Clerk / Auth0 / custom)
2. Client asks YOUR backend for a CometChat auth token
↓ (POST /api/cometchat-token { uid })
3. Backend calls CometChat REST API → gets an Auth Token for that UID
↓ POST https://{APP_ID}.api-{REGION}.cometchat.io/v3/users/{uid}/auth_tokens
with header apiKey: <REST_API_KEY>
4. Client calls CometChatUIKit.login({ authToken: "..." })
```
The RN client never sees the REST API Key. The server never ships a password or email to the client. The CometChat SDK holds the auth token, not a static key.
---
## 4. Server endpoint recipes
RN projects don't have Next.js-style API routes. You need a separate backend. Pick the one the user already has, or the simplest if they're starting fresh.
### 4a. Express (Node.js backend)
```ts
// server/routes/cometchat-token.ts
import { Router } from "express";
import { requireAuth } from "../middleware/auth"; // your existing auth
const router = Router();
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
router.post("/cometchat-token", requireAuth, async (req, res) => {
// Derive UID from authenticated session — NOT from the request body in prod.
const uid = req.user.id;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({}),
},
);
if (!r.ok) {
const error = await r.text();
console.error("CometChat token error:", error);
return res.status(r.status).json({ error: "Failed to generate auth token" });
}
const data = await r.json();
return res.json({ authToken: data.data.authToken });
});
export default router;
```
### 4b. Hono (Cloudflare Workers / Bun / Node)
```ts
// server/cometchat-token.ts
import { Hono } from "hono";
const app = new Hono();
app.post("/api/cometchat-token", async (c) => {
const user = c.get("user"); // your middleware-resolved user
if (!user) return c.json({ error: "unauthorized" }, 401);
const APP_ID = c.env.COMETCHAT_APP_ID;
const REGION = c.env.COMETCHAT_REGION;
const REST_API_KEY = c.env.COMETCHAT_REST_API_KEY;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(user.id)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({}),
},
);
if (!r.ok) return c.json({ error: "token mint failed" }, 502);
const data = await r.json();
return c.json({ authToken: data.data.authToken });
});
export default app;
```
### 4c. Firebase Cloud Functions
```ts
// functions/src/cometchat-token.ts
import { onCall, HttpsError } from "firebase-functions/v2/https";
export const getCometChatToken = onCall(
{ secrets: ["COMETCHAT_APP_ID", "COMETCHAT_REGION", "COMETCHAT_REST_API_KEY"] },
async (request) => {
if (!request.auth) throw new HttpsError("unauthenticated", "Sign in required");
const uid = request.auth.uid;
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({}),
},
);
if (!r.ok) throw new HttpsError("internal", "token mint failed");
const data = await r.json();
return { authToken: data.data.authToken };
},
);
```
Client call:
```tsx
import functions from "@react-native-firebase/functions";
const result = await functions().httpsCallable("getCometChatToken")();
const authToken = result.data.authToken;
```
### 4d. Vercel Serverless / Next.js API Route
Even if the RN app isn't Next.js, the user's existing web app often is. Reuse the same backend:
```ts
// pages/api/cometchat-token.ts (or app/api/cometchat-token/route.ts)
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "./auth/[...nextauth]";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(405).end();
const session = await getServerSession(req, res, authOptions);
if (!session?.user) return res.status(401).json({ error: "unauthorized" });
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
const uid = session.user.id;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: { "Content-Type": "application/json", appId: APP_ID, apiKey: REST_API_KEY },
body: JSON.stringify({}),
},
);
if (!r.ok) return res.status(502).json({ error: "token mint failed" });
const data = await r.json();
return res.json({ authToken: data.data.authToken });
}
```
---
## 5. Client-side: `CometChatUIKit.login({ authToken })`
Once the server is serving tokens, update the RN client to fetch the token and use it. This is a change to the `CometChatProvider` (see `cometchat-native-core` § 6) — swap the `uid` prop for an `authToken` prop.
### 5a. Update the provider to support authToken
```tsx
// CometChatProvider.tsx — production-aware version
import React, { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import {
CometChatUIKit,
UIKitSettingsBuilder,
} from "@cometchat/chat-uikit-react-native";
let initialized = false;
let loginInFlight: Promise<unknown> | null = null;
async function ensureLoggedIn(authToken?: string, uid?: string): Promise<void> {
const existing = await CometChatUIKit.getLoggedinUser();
if (existing) return;
if (loginInFlight) {
await loginInFlight;
return;
}
// Production — prefer authToken
if (authToken) {
loginInFlight = CometChatUIKit.login({ authToken });
} else if (uid) {
loginInFlight = CometChatUIKit.login({ uid }); // dev fallback
} else {
return; // nothing to log in with yet
}
try {
await loginInFlight;
} finally {
loginInFlight = null;
}
}
interface Props {
appId: string;
region: string;
authKey?: string; // dev only; omit in production
authToken?: string; // production — from your backend
uid?: string; // dev only
children: ReactNode;
}
export function CometChatProvider({ appId, region, authKey, authToken, uid, children }: Props) {
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function setup() {
try {
if (!initialized) {
initialized = true;
const builder = new UIKitSettingsBuilder()
.setAppId(appId)
.setRegion(region)
.subscribePresenceForAllUsers();
if (authKey) builder.setAuthKey(authKey);
await CometChatUIKit.init(builder.build());
}
await ensureLoggedIn(authToken, uid);
setIsReady(true);
} catch (e) {
setError(String(e));
}
}
setup();
}, [appId, region, authKey, authToken, uid]);
if (!isReady) return null;
return <>{children}</>;
}
```
**Push registration lands here** — right after `ensureLoggedIn` resolves,
before `setIsReady(true)`. The CometChat SDK scopes push tokens to the
logged-in user, so registering before login associates the token with
"anonymous" and the device won't receive pushes.
```tsx
import { bootstrapPushAfterLogin } from "../push/bootstrap";
//...
await ensureLoggedIn(authToken, uid);
await bootstrapPushAfterLogin(); // registers FCM/APNs token with CometChat
setIsReady(true);
```
And unregister BEFORE `CometChatUIKit.logout()` — the SDK needs the user
context to dissociate the token. See `cometchat-native-push § 7` for
the full `bootstrapPushAfterLogin` / `unregisterPushTokenOnLogout` helper
pair.
### 5b. Fetch the token from your backend
Typical app flow:
```tsx
// App.tsx
import { useState, useEffect } from "react";
import { CometChatProvider } from "./src/providers/CometChatProvider";
import { useMyAppAuth } from "./src/hooks/useMyAppAuth"; // your existing auth
export default function App() {
const { user, isAuthenticated } = useMyAppAuth();
const [cometChatToken, setCometChatToken] = useState<string | null>(null);
useEffect(() => {
if (!isAuthenticated) {
setCometChatToken(null);
return;
}
// Fetch a CometChat auth token from your backend
fetch("https://api.yourapp.com/cometchat-token", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${user.jwt}` },
})
.then((r) => r.json())
.then((data) => setCometChatToken(data.authToken))
.catch((e) => console.error("CometChat token fetch failed:", e));
}, [isAuthenticated, user?.jwt]);
if (!isAuthenticated) return <LoginScreen />;
if (!cometChatToken) return <LoadingScreen message="Connecting chat..." />;
return (
<CometChatProvider
appId={COMETCHAT_APP_ID}
region={COMETCHAT_REGION}
authToken={cometChatToken}
// no authKey prop in production
>
<AppNavigator />
</CometChatProvider>
);
}
```
### 5c. Handle token expiry / refresh
Auth tokens have a configurable TTL (default 24 hours). On token expiry, SDK calls start failing. Handle this by re-minting on 401:
```tsx
useEffect(() => {
const LISTENER_ID = "TOKEN_EXPIRY_LISTENER";
CometChat.addConnectionListener(
LISTENER_ID,
new CometChat.ConnectionListener({
onDisconnected: async () => {
// Connection dropped. Token might be expired.
// Re-fetch and re-login.
const freshToken = await fetchCometChatToken(user.jwt);
setCometChatToken(freshToken);
await CometChatUIKit.login({ authToken: freshToken });
},
}),
);
return () => CometChat.removeConnectionListener(LISTENER_ID);
}, [user?.jwt]);
```
A simpler approach for apps that can tolerate a forced re-login: on any 401 from the SDK, log the user out and force them through your app's sign-in flow again.
---
## 6. User management CRUD
When someone signs up in your app, you need to create a matching CometChat user. Same for profile updates (name/avatar change) and deletion. These happen on your backend, using the REST API with the REST API Key.
### 6a. Create a user on signup
```ts
async function createCometChatUser(uid: string, name: string, avatarUrl?: string) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({
uid,
name,
avatar: avatarUrl,
}),
},
);
if (!r.ok) throw new Error(`CometChat user create failed: ${await r.text()}`);
return r.json();
}
```
### 6b. Update a user on profile change
```ts
async function updateCometChatUser(uid: string, updates: Partial<{ name: string; avatar: string; metadata: any }>) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify(updates),
},
);
if (!r.ok) throw new Error(`CometChat user update failed: ${await r.text()}`);
return r.json();
}
```
### 6c. Delete a user on account deletion
```ts
async function deleteCometChatUser(uid: string) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({ permanent: true }),
},
);
if (!r.ok) throw new Error(`CometChat user delete failed: ${await r.text()}`);
}
```
### 6d. Where to wire these calls
The CRUD functions live on your backend; you call them from your existing auth event handlers:
| Auth event | When to call | Function |
|---|---|---|
| User signs up | After your app's user creation succeeds | `createCometChatUser(newUser.id, newUser.name, newUser.avatarUrl)` |
| User updates name or avatar | After your app's profile update succeeds | `updateCometChatUser(user.id, { name, avatar })` |
| User deletes account | Before/after your app's user deletion | `deleteCometChatUser(user.id)` |
---
## 7. Auth-provider integration recipes
Your RN app's auth layer typically comes from one of these SDKs. How to wire CometChat into each:
### 7a. Firebase Auth
Firebase issues a UID per user. Use that same UID in CometChat.
```ts
// Backend (Firebase Cloud Function)
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { onUserCreated, onUserDeleted } from "firebase-functions/v2/auth";
export const onSignup = onUserCreated(async (event) => {
const { uid, displayName, photoURL } = event.data;
await createCometChatUser(uid, displayName ?? "User", photoURL);
});
export const onAccountDelete = onUserDeleted(async (event) => {
await deleteCometChatUser(event.data.uid);
});
// Profile updates are app-level — hook into your profile-update handler
```
Client — get the Firebase ID token, send to your `cometchat-token` endpoint:
```tsx
import auth from "@react-native-firebase/auth";
const idToken = await auth().currentUser!.getIdToken();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${idToken}` },
});
const { authToken } = await r.json();
```
### 7b. Supabase Auth
Supabase also issues a UID. Use it as the CometChat UID.
```ts
// Backend — Supabase Edge Function triggered on signup
Deno.serve(async (req) => {
const event = await req.json();
if (event.type === "INSERT" && event.table === "users") {
const { id, email } = event.record;
await createCometChatUser(id, email.split("@")[0]);
}
return new Response("ok");
});
```
Client:
```tsx
import { supabase } from "./supabase";
const { data: { session } } = await supabase.auth.getSession();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${session!.access_token}` },
});
```
### 7c. Clerk Expo
Clerk is Expo-friendly and has its own webhooks for user lifecycle events.
```tsx
// Client — React Native
import { useAuth } from "@clerk/clerk-expo";
const { getToken, userId } = useAuth();
const jwt = await getToken();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${jwt}` },
});
```
Backend — use a Clerk webhook to trigger CRUD on user lifecycle events.
### 7d. Auth0
```tsx
import { useAuth0 } from "react-native-auth0";
const { getCredentials } = useAuth0();
const { accessToken } = await getCredentials();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}` },
});
```
Backend — use Auth0 Actions or Rules to trigger CRUD webhooks.
### 7e. Custom JWT / bespoke auth
If the user's auth is custom (their own JWT), the pattern is the same: client includes `Authorization: Bearer <jwt>`, server validates + extracts UID + mints CometChat auth token.
---
## 8. Environment variables — split between client + server
| Variable | Location | Visibility |
|---|---|---|
| `COMETCHAT_APP_ID` | Client AND server | OK client-side |
| `COMETCHAT_REGION` | Client AND server | OK client-side |
| `COMETCHAT_AUTH_KEY` | **Dev client only.** Remove from production. | Should NEVER ship in a production RN bundle |
| `COMETCHAT_REST_API_KEY` | **Server only.** Your backend's env. | Never ships to client, ever |
| `COMETCHAT_TOKEN_ENDPOINT` | Client | Your backend URL (e.g. `https://api.yourapp.com/cometchat-token`) — safe in client bundle |
### Production RN client `.env` (or app.json extra):
```
COMETCHAT_APP_ID=your_app_id
COMETCHAT_REGION=us
COMETCHAT_TOKEN_ENDPOINT=https://api.yourapp.com/cometchat-token
# No COMETCHAT_AUTH_KEY in production
# No COMETCHAT_REST_API_KEY — server-only
```
### Server `.env`:
```
COMETCHAT_APP_ID=your_app_id
COMETCHAT_REGION=us
COMETCHAT_REST_API_KEY=your_rest_api_key
```
---
## 9. Security checklist
Before releasing to production, verify:
- [ ] `COMETCHAT_AUTH_KEY` removed from client `.env` / `app.json extra` / any `EXPO_PUBLIC_*` var
- [ ] Production provider uses `authToken` prop, not `authKey`
- [ ] `COMETCHAT_REST_API_KEY` lives only on your backend (check with `grep -r REST_API_KEY src/`)
- [ ] Token endpoint is behind auth — unauthenticated users can't mint a token for an arbitrary UID
- [ ] UID derivation on the token endpoint comes from the authenticated session, NOT from the request body (otherwise anyone can mint a token for anyone)
- [ ] Rate limit on the token endpoint (prevents abuse)
- [ ] HTTPS-only — no HTTP in production
- [ ] User CRUD endpoints are authenticated (or called from webhooks with signature verification)
- [ ] CometChat user deletion happens on account deletion (GDPR / privacy compliance)
---
## 10. Rate limits + retry
CometChat's REST API has rate limits per app. For the token endpoint:
- Default: 100 requests/minute per app
- Token generation is cheap — if you're hitting limits, you're likely minting too often (e.g. one mint per RN screen mount). Mint once per sign-in, cache client-side, reuse until expiry.
Retry policy:
- 5xx — retry with exponential backoff (1s, 2s, 4s, give up)
- 4xx — do NOT retry. Surface the error.
---
## 11. Anti-patterns
1. **NEVER ship the REST API Key in an RN bundle.** Not under any env-var name or prefix. Not in `app.json extra`. Not in `EXPO_PUBLIC_*`. Not in a .gitignored file the user commits by accident. If you see yourself writing an env var for `REST_API_KEY` in a client-side config, stop.
2. **NEVER let the client specify the UID to mint a token for.** The server must derive UID from the authenticated session. A `POST /cometchat-token { uid: "..." }` that trusts the body is equivalent to no auth — anyone can impersonate anyone.
3. **Don't cache the auth token to disk forever.** It expires. Either re-mint on every cold start or store with a short TTL and refresh on 401.
4. **Don't use `login({ uid })` in production.** `uid` mode requires an Auth Key on the UIKit settings. In production you should set neither `authKey` on the UIKitSettings nor call `login({ uid })` — both are dev-only patterns.
5. **Don't forget user CRUD.** A user who signs up in your app but has no matching CometChat user will get "user does not exist" errors on `login({ authToken })`. The token endpoint mints tokens, but the user must already exist in CometChat.
6. **Don't skip the security checklist.** Production bugs in auth are catastrophic.
7. **Don't retry 4xx errors.** Token-endpoint 400s are config mistakes (wrong REST API Key, malformed UID, etc.). Retrying makes it worse.
---
## 12. Verifying production auth works
1. Build a production-configuration version of the app (no Auth Key, only AppId + Region + token endpoint).
2. Log in as a real user through your app's normal flow.
3. In the RN debugger network tab, confirm `POST /cometchat-token` returns `{ authToken: "..." }`.
4. Confirm `CometChatUIKit.login({ authToken })` resolves.
5. Send a message — verify delivery.
6. Force-close the app, reopen — token fetch + login should happen again on cold start.
7. (Optional) Wait out the token TTL (default 24hr), verify the 401-refresh path works.
If any step fails, see `cometchat-native-troubleshooting` § Auth / Token issues.
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Init / login / provider wrapper chain — prerequisite |
| `cometchat-native-components` | The base component props (nothing production-specific there) |
| `cometchat-native-placement` | Where your chat UI goes (no change in production) |
| `cometchat-native-expo-patterns` | Expo-specific env var wiring (`expo-constants` vs `EXPO_PUBLIC_*`) |
| `cometchat-native-bare-patterns` | Bare RN env var wiring (`react-native-config`) |
| `cometchat-native-theming` | Theme customization (independent of auth) |
| `cometchat-native-features` | Feature flags (polls, extensions, etc. — all still work in prod) |
| `cometchat-native-customization` | If customization depends on server-side data (user tags, metadata) |
| `cometchat-native-production` | This skill — server tokens + user CRUD |
| `cometchat-native-troubleshooting` | 401 on token fetch, "user does not exist" on login, token-endpoint rate limit |
---
name: cometchat-native-push
description: "Push notifications for React Native CometChat — APNs + FCM setup, dashboard provider configuration, client registration, token lifecycle, foreground display, background wake, tap-to-deep-link, and the Expo Go / APNs-environment traps that silently break production."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5; @cometchat/chat-sdk-react-native ^4; @react-native-firebase/messaging ^18"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory"
metadata:
author: "CometChat"
version: "1.0.0"
tags: "cometchat react-native push notifications apns fcm firebase expo"
---
## Purpose
Teaches Claude how to add push notifications to a CometChat React Native integration — end-to-end, from Apple Developer / Google Cloud setup through CometChat dashboard provider configuration, client token registration, foreground/background handling, and tap-to-deep-link.
**Push is non-negotiable for production chat.** Without it, a backgrounded app never wakes when a message arrives. The user doesn't see the message, doesn't re-open the app, and stops using chat. This is THE feature that separates "works in demo" from "works in production."
Ground truth: `examples/SampleAppWithPushNotifications/` in `@cometchat/chat-uikit-react-native@5.3.3`, `docs/sdk/react-native/push-notification-setup.mdx`, and `https://www.cometchat.com/docs/notifications/push-integration`.
---
## 1. The moving pieces
Push spans four systems that must all agree:
```
┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────┐
│ Apple / │ │ CometChat │ │ CometChat │ │ RN │
│ Google │ → │ Dashboard │ → │ server │ → │ client │
│ (APNs/FCM) │ │ (providers) │ │ (via SDK) │ │ (app) │
└─────────────┘ └─────────────┘ └──────────────┘ └────────┘
p8 key / JSON Uploaded creds Webhook on message Displays notif
```
When user A sends a message to user B:
1. CometChat server receives the message
2. Looks up B's registered push tokens (client did this at login)
3. Sends a push via APNs (iOS) or FCM (Android) using the credentials the dashboard holds
4. B's device receives it, OS wakes the app (or fires foreground handler)
5. Notification displays; tap → app navigates to the conversation
All five steps must work. A broken step is almost always silent — no log, no error, just no notification. Debugging requires checking each layer.
---
## 2. Expo Go CANNOT receive push notifications
**This is the #1 support ticket from Expo users.** Expo Go is a prebuilt shell app without your custom native modules — it has no APNs entitlement, no FCM configuration, no way to receive your app's push.
**For push, Expo projects require a development build:**
```bash
npx expo install expo-dev-client
npx expo prebuild --clean # generates ios/ + android/ with native configuration
eas build --profile development --platform ios # or android
```
Open the resulting `.ipa` / `.apk` and run `npx expo start --dev-client`. This is the only Expo setup that can receive push.
If a user reports "I set everything up but no notifications arrive" and they're running Expo Go, that's the answer — no code fix will help.
---
## 3. APNs setup (iOS)
### 3a. Create an APNs Auth Key (p8)
Apple's two options for signing push — certificate (`.p12`) or auth key (`.p8`). Use `.p8`. It never expires, one key works for all your apps, and CometChat accepts the simpler key format.
1. https://developer.apple.com/account → **Certificates, Identifiers & Profiles** → **Keys** → "+"
2. Name it (e.g., "CometChat APNs"), check **Apple Push Notifications service (APNs)**, Continue, Register
3. **Download the `.p8` file** (one-time — Apple never lets you download it again)
4. Copy the **Key ID** (10-char alphanumeric, shown on the key page)
5. From the membership page, copy your **Team ID** (10-char alphanumeric, top-right)
6. Collect your app's **Bundle ID** (from `ios/<Name>.xcodeproj` → Targets → General)
You'll paste all four into the CometChat dashboard in §5.
### 3b. Enable Push Notifications capability in Xcode
```
Open ios/<Name>.xcworkspace
Select the project → Signing & Capabilities tab
Click "+ Capability" → "Push Notifications"
Click "+ Capability" → "Background Modes"
In Background Modes, check:
- Remote notifications
- Voice over IP (only if integrating CometChat calls)
```
This writes `aps-environment` (development or production) into the entitlements file. Wrong environment is the #1 silent-failure in §10.
### 3c. Two environments — the TestFlight / App Store trap
APNs has two parallel networks:
- **Development** (`aps-environment: development`) — Xcode dev builds. Uses dev key paths.
- **Production** (`aps-environment: production`) — TestFlight, App Store, Ad-Hoc. Uses prod key paths.
The p8 auth key you generated in 3a works for **both** environments. But CometChat has to know which environment the token came from. If you upload the p8 only as "Development" in the dashboard, TestFlight builds silently fail — a token arrives from production APNs but the dashboard has no matching credentials.
**Fix:** upload the same p8 twice in the CometChat dashboard — once as Development provider, once as Production provider. Then register with the matching provider ID at runtime (§7).
---
## 4. FCM setup (Android)
### 4a. Create a Firebase project + service account
1. https://console.firebase.google.com → **Add project** → name it, continue through setup
2. **Project Settings** (gear icon) → **Service accounts** tab
3. **Generate new private key** → downloads a `.json` file with your server credentials
This JSON file is what CometChat's dashboard needs.
### 4b. Add Android app to Firebase + download google-services.json
1. Project Overview → **Add app** → Android
2. Enter your app's **package name** (from `android/app/build.gradle` → `applicationId`)
3. Download `google-services.json`
4. Place it at `android/app/google-services.json`
5. Add this line at the end of `android/app/build.gradle`:
```gradle
apply plugin: 'com.google.gms.google-services'
```
6. In `android/build.gradle` under `buildscript.dependencies`:
```gradle
classpath 'com.google.gms:google-services:4.4.2'
```
**Expo managed:** `google-services.json` goes in the project root, and you reference it in `app.json`:
```json
{
"expo": {
"android": {
"googleServicesFile": "./google-services.json"
},
"plugins": ["@react-native-firebase/app", "@react-native-firebase/messaging"]
}
}
```
### 4c. iOS Firebase config (if using firebase/messaging on iOS)
`react-native-firebase/messaging` wraps APNs under the hood on iOS, so the APNs setup in §3 is what actually powers iOS push. BUT Firebase expects a `GoogleService-Info.plist` even though it doesn't route iOS push through FCM:
1. Add iOS app in Firebase console (Project Overview → Add app → iOS)
2. Download `GoogleService-Info.plist`
3. Add it to `ios/<Name>/` via Xcode (Right-click project → Add Files)
4. In Expo: put it at project root and reference in `app.json`:
```json
{ "expo": { "ios": { "googleServicesFile": "./GoogleService-Info.plist" } } }
```
---
## 5. CometChat dashboard — upload credentials
https://app.cometchat.com → your app → **Notifications** → **Push Notifications**
### 5a. Add an APNs provider (per environment)
- **Add Provider** → choose APNs
- Provider name: `apns-dev` (or similar)
- Environment: **Development**
- Upload the `.p8` file from §3a
- Paste Key ID, Team ID, Bundle ID
- Save → copy the **Provider ID** string (you'll need it in §7)
Repeat for Production:
- Provider name: `apns-prod`
- Environment: **Production**
- Same p8, Key ID, Team ID, Bundle ID
- Save → copy the second Provider ID
If you skip the production provider, TestFlight / App Store builds will silently not receive push.
### 5b. Add an FCM provider
- **Add Provider** → choose FCM
- Provider name: `fcm-default`
- Upload the service account `.json` file from §4a
- Save → copy the Provider ID
### 5c. Cache the Provider IDs
You'll have 2-3 provider IDs. Store them in a config constant in your app:
```ts
// src/config/push.ts
export const PUSH_PROVIDERS = {
fcm: "fcm-<hex-from-dashboard>",
apnsDev: "apns-dev-<hex-from-dashboard>",
apnsProd: "apns-prod-<hex-from-dashboard>",
};
```
At runtime (§7), you'll pick the right one based on platform + `__DEV__`.
---
## 6. Install client packages
### Bare React Native
```bash
npm install @react-native-firebase/app @react-native-firebase/messaging \
@notifee/react-native @react-native-community/push-notification-ios
cd ios && pod install && cd ..
```
- `@react-native-firebase/app` — initializes Firebase (reads GoogleService-Info.plist / google-services.json)
- `@react-native-firebase/messaging` — FCM on Android AND APNs on iOS (Firebase handles both)
- `@notifee/react-native` — local notification display (for foreground messages on Android; required because FCM data-only pushes don't auto-display)
- `@react-native-community/push-notification-ios` — iOS APNs device token retrieval + notification tap handling (`getInitialNotification`, `addEventListener`)
### Expo managed (dev build)
```bash
npx expo install @react-native-firebase/app @react-native-firebase/messaging \
@notifee/react-native @react-native-community/push-notification-ios expo-dev-client
npx expo prebuild --clean
```
Then build a dev client (§2) and run `npx expo start --dev-client`.
**iOS permissions in `ios/<Name>/Info.plist`:** no changes needed for basic push.
**Android permissions in `android/app/src/main/AndroidManifest.xml`:**
```xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
```
For Expo managed, put permissions in `app.json`:
```json
{
"expo": {
"android": {
"permissions": ["POST_NOTIFICATIONS", "WAKE_LOCK"]
}
}
}
```
---
## 7. Register the push token with CometChat
The canonical API — confirmed from `examples/SampleAppWithPushNotifications/src/utils/PushNotification.tsx`:
```ts
import { CometChatNotifications } from "@cometchat/chat-sdk-react-native";
```
`CometChatNotifications.PushPlatforms` enum values:
| Platform | Enum value |
|---|---|
| Android (FCM) | `FCM_REACT_NATIVE_ANDROID` |
| iOS (FCM — Firebase proxies APNs) | `FCM_REACT_NATIVE_IOS` |
| iOS (APNs direct, non-VoIP) | `APNS_REACT_NATIVE_DEVICE` |
| iOS (APNs VoIP — calls only) | `APNS_REACT_NATIVE_VOIP` |
Most apps use FCM on both platforms (simpler — Firebase handles the APNs dance). Only use `APNS_REACT_NATIVE_DEVICE` if you're registering the raw APNs device token without Firebase in between.
### 7a. Canonical register helper
```ts
// src/push/registerPushToken.ts
import { Platform } from "react-native";
import { CometChatNotifications } from "@cometchat/chat-sdk-react-native";
import { PUSH_PROVIDERS } from "../config/push";
export async function registerPushToken(token: string): Promise<void> {
const platform =
Platform.OS === "android"
? CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID
: CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_IOS;
// Single FCM provider covers both platforms when using firebase/messaging.
const providerId = PUSH_PROVIDERS.fcm;
try {
await CometChatNotifications.registerPushToken(token, platform, providerId);
} catch (err) {
console.error("[push] registerPushToken failed", err);
}
}
```
### 7b. Fetch the FCM token and register (after login)
```ts
// src/push/bootstrap.ts
import messaging from "@react-native-firebase/messaging";
import { registerPushToken } from "./registerPushToken";
export async function bootstrapPushAfterLogin(): Promise<void> {
await messaging().registerDeviceForRemoteMessages();
const token = await messaging().getToken();
await registerPushToken(token);
// Re-register when the token rotates (rare but it happens — new install, app restore).
messaging().onTokenRefresh(async (newToken) => {
await registerPushToken(newToken);
});
}
```
Call `bootstrapPushAfterLogin()` in the same effect that runs `CometChatUIKit.login()`. Order matters — the SDK needs a logged-in user to associate the token with.
### 7c. Unregister on logout
```ts
import { CometChatNotifications } from "@cometchat/chat-sdk-react-native";
export async function unregisterPushTokenOnLogout(): Promise<void> {
try {
await CometChatNotifications.unregisterPushToken();
} catch (err) {
console.error("[push] unregisterPushToken failed", err);
}
}
```
Call this BEFORE `CometChatUIKit.logout()` — after logout the SDK can't resolve the user to unregister the token against.
If the user switches accounts without logging out (bad pattern, but it happens), re-register with the new user after login. CometChat auto-scopes push to the current user.
---
## 8. Permissions — ask early, handle deny gracefully
### iOS
iOS prompts the user on the first call to `messaging().requestPermission()`. Do it early in the onboarding flow (post-login is fine), not in the first render — a permission prompt on app open looks hostile.
```ts
import messaging from "@react-native-firebase/messaging";
async function requestIosPush(): Promise<boolean> {
const status = await messaging().requestPermission();
return (
status === messaging.AuthorizationStatus.AUTHORIZED ||
status === messaging.AuthorizationStatus.PROVISIONAL
);
}
```
### Android
Android 13+ requires the `POST_NOTIFICATIONS` runtime permission (prior versions grant it automatically from the manifest).
```ts
import { PermissionsAndroid, Platform } from "react-native";
async function requestAndroidPush(): Promise<boolean> {
if (Platform.OS !== "android" || Platform.Version < 33) return true;
const status = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);
return status === PermissionsAndroid.RESULTS.GRANTED;
}
```
### Handle deny
If the user denies, don't retry — it just shows the system "open Settings" prompt. Surface a small UI nudge in chat settings: "Enable push notifications — so you know when you get a message." Link to `Linking.openSettings()`.
---
## 9. Display, background, tap
### 9a. Foreground messages (Android)
FCM on Android delivers data-only pushes while the app is foregrounded — the OS does NOT display them automatically. You have to render a local notification with `@notifee/react-native`:
```ts
import messaging from "@react-native-firebase/messaging";
import notifee, { AndroidImportance } from "@notifee/react-native";
messaging().onMessage(async (remoteMessage) => {
const { title, body } = remoteMessage.data ?? {};
const channelId = await notifee.createChannel({
id: "chat-messages",
name: "Chat Messages",
importance: AndroidImportance.HIGH,
});
await notifee.displayNotification({
title: title ?? "New Message",
body: body ?? "You received a new message.",
android: { channelId, pressAction: { id: "default" } },
data: remoteMessage.data, // preserved for tap handling
});
});
```
iOS foregrounding behavior is configured by `messaging().setForegroundNotificationPresentationOptions(...)` — set it once at app startup. Default is "don't display" (iOS assumes the app handles it), so you must opt-in to badges/banners/sounds.
### 9b. Background / killed messages
OS-delivered, no code needed. The notification displays as an OS notification. Tap handling: see §9c.
### 9c. Tap to deep-link
Three scenarios to handle:
**iOS — app killed, tap opens the app:**
```ts
import PushNotificationIOS from "@react-native-community/push-notification-ios";
async function checkInitialNotificationIOS(): Promise<void> {
const notification = await PushNotificationIOS.getInitialNotification();
if (!notification) return;
const data = notification.getData();
navigateFromPayload(data);
}
```
**iOS — app in background, tap foregrounds:**
```ts
PushNotificationIOS.addEventListener("notification", (notification) => {
const data = notification.getData();
if (data.userInteraction === 1) {
navigateFromPayload(data);
}
notification.finish(PushNotificationIOS.FetchResult.NoData);
});
```
**Android (via messaging):**
```ts
import messaging from "@react-native-firebase/messaging";
// App killed → tap → opens app
messaging().getInitialNotification().then((remoteMessage) => {
if (remoteMessage?.data) navigateFromPayload(remoteMessage.data);
});
// App backgrounded → tap → foregrounds
messaging().onNotificationOpenedApp((remoteMessage) => {
if (remoteMessage?.data) navigateFromPayload(remoteMessage.data);
});
// Foreground local notification (displayed via notifee in §9a) → tap
import notifee, { EventType } from "@notifee/react-native";
notifee.onForegroundEvent(({ type, detail }) => {
if (type === EventType.PRESS) {
navigateFromPayload(detail.notification?.data ?? {});
}
});
```
### 9d. Payload → navigation
CometChat's push payload schema (confirmed from `examples/SampleAppWithPushNotifications/src/utils/helper.ts`):
```ts
{
type: "chat",
receiverType: "user" | "group",
sender: "<uid>",
receiver: "<uid-or-guid>",
conversationId: "<compound-id>",
unreadMessageCount: "<number-as-string>",
title: "Alice",
body: "Hey, you around?",
senderAvatar: "<url>",
tag: "<messageId>",
message: "<JSON-stringified-full-message>", // parse for parentId, id, etc.
}
```
Parse it into navigation params:
```ts
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { navigate } from "./NavigationService"; // createNavigationContainerRef wrapper
async function navigateFromPayload(data: Record<string, unknown>): Promise<void> {
if (data.type !== "chat") return;
let parentMessageId: string | undefined;
if (typeof data.message === "string") {
try {
parentMessageId = JSON.parse(data.message).parentId;
} catch {}
}
if (data.receiverType === "group" && typeof data.receiver === "string") {
const group = await CometChat.getGroup(data.receiver);
navigate("Messages", { group, ...(parentMessageId ? { parentMessageId } : {}) });
} else if (data.receiverType === "user" && typeof data.sender === "string") {
const user = await CometChat.getUser(data.sender);
navigate("Messages", { user, ...(parentMessageId ? { parentMessageId } : {}) });
}
}
```
Use React Navigation's `createNavigationContainerRef` so you can navigate from outside React (the tap handler fires before the component tree mounts on app launch):
```ts
// src/navigation/NavigationService.ts
import { createNavigationContainerRef } from "@react-navigation/native";
export const navigationRef = createNavigationContainerRef();
export function navigate(name: string, params?: unknown): void {
if (navigationRef.isReady()) navigationRef.navigate(name as never, params as never);
else setTimeout(() => navigate(name, params), 100); // wait for mount
}
```
In the root layout:
```tsx
<NavigationContainer ref={navigationRef}>…</NavigationContainer>
```
For Expo Router, use the `router` from `expo-router` inside the tap handler — no navigation ref needed, but check `router.canGoBack()` before pushing on cold start.
---
## 10. Badge count
CometChat sends `unreadMessageCount` in the payload. Set it on iOS via `PushNotificationIOS.setApplicationIconBadgeNumber(count)` inside the notification handler. On Android, Notifee's `setBadgeCount()` works on most launchers but is inconsistent (Samsung, Xiaomi have their own rules).
Reset badge to 0 when the user opens a conversation:
```ts
import { AppState, Platform } from "react-native";
import PushNotificationIOS from "@react-native-community/push-notification-ios";
import notifee from "@notifee/react-native";
function clearBadge(): void {
if (Platform.OS === "ios") PushNotificationIOS.setApplicationIconBadgeNumber(0);
else notifee.setBadgeCount(0);
}
```
---
## 11. Testing the push pipeline
End-to-end verification — run each step in order and stop at the first failure. Silent failures are the norm, so testing each layer separately is faster than chasing a black box.
1. **APNs alone** (iOS): In Firebase Console → Cloud Messaging → Send test message to your FCM token. If this arrives, Firebase + APNs are fine.
2. **FCM alone** (Android): Same as above — Firebase test message. If it arrives, FCM + `google-services.json` are fine.
3. **CometChat provider → device**: Send a message to the logged-in user from another user. If this fails but step 1/2 worked, the issue is in the CometChat dashboard provider config (wrong p8 environment, wrong bundle ID, expired service account).
4. **Tap deep-links correctly**: Put the app in background, send a message, tap the notification. App should land on the right conversation.
5. **TestFlight / App Store**: Build an archive, upload to TestFlight, install on a real device (NOT the simulator — iOS simulator cannot receive real APNs). Repeat step 3. This is where the "production APNs provider not uploaded" trap appears.
---
## 12. Troubleshooting — common silent failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Dev works, TestFlight doesn't | Production APNs provider not uploaded OR `aps-environment` is still `development` in release build | Upload prod p8 provider (§5a). Check `ios/<Name>/<Name>.entitlements` has `aps-environment: production` for Release config |
| No iOS simulator notifications | Simulator can't receive real APNs | Use a real device |
| `requestPermission()` never prompts | Already denied — prompt won't re-show | `Linking.openSettings()` and tell user to toggle Notifications on |
| Token prints but no push arrives | Token registered BEFORE login | Call `registerPushToken` AFTER `CometChatUIKit.login` resolves |
| Android foreground: OS notif shows | FCM delivered as notification + data (CometChat sends data-only now); old app code | Update to firebase/messaging ≥18 — old versions force auto-display |
| Android foreground: nothing shows | No `onMessage` handler OR no notifee channel created | Add `messaging().onMessage` + `notifee.createChannel` (§9a) |
| "Default FirebaseApp is not initialized" | google-services.json missing or build plugin not applied | Re-check §4b. Clean build: `cd android && ./gradlew clean` |
| Notification tap doesn't navigate | NavigationContainer not ready when tap handler fires | Use the `setTimeout` retry pattern in `navigate()` (§9d) |
| Expo app receives nothing in Expo Go | Expo Go can't receive push | Build a dev client (§2) |
| Token refreshes but CometChat still uses old | `onTokenRefresh` listener not wired | Wire in `bootstrapPushAfterLogin` (§7b) |
| Works for User A but not User B after logout | `unregisterPushToken` not called on logout | Call BEFORE `logout()` (§7c) |
| iOS push arrives but `data` is empty | Payload is APS-only (no `content-available`) — CometChat default is correct; check if custom template stripped data | Check dashboard → Notifications → Template |
---
## 13. Hard rules
- **Register AFTER login.** The SDK needs a logged-in user to scope the token. Register before login and the token lands against "anonymous."
- **Unregister BEFORE logout.** The SDK needs to know the user to dissociate the token.
- **Call `onTokenRefresh`.** FCM rotates tokens. Missing the rotation means push stops working after a few weeks for some users.
- **Upload both APNs environments.** Dev + Production. Missing Production = silent TestFlight/App Store breakage.
- **Expo Go is a dead-end.** Build a dev client for push. No exceptions.
- **Don't auto-display on Android foreground.** Show a local notification via Notifee so the user sees the message.
- **Don't ship without testing on a real device.** iOS simulator doesn't receive real APNs.
---
## 14. Skill routing
| This skill | Covers |
|---|---|
| `cometchat-native-push` (this) | APNs + FCM + CometChat dashboard + client registration + tap handling |
| `cometchat-native-core` | Init, login, four-wrapper chain, login concurrency guard |
| `cometchat-native-features` | Calling SDK (`@cometchat/calls-sdk-react-native`) — push for VoIP is a separate channel (use `APNS_REACT_NATIVE_VOIP` + `react-native-voip-push-notification`) |
| `cometchat-native-production` | Server-minted auth tokens + user CRUD. Register push only after login; if using auth tokens, register after `login({ authToken })` |
| `cometchat-native-bare-patterns` | `pod install` + Xcode capability steps |
| `cometchat-native-expo-patterns` | `expo prebuild` + dev client setup |
| `cometchat-native-troubleshooting` | Metro cache, Podfile.lock, Android Maven. Push-specific symptoms are in §12 here |
---
name: cometchat-native-testing
description: "Testing patterns for CometChat React Native — Jest + React Native Testing Library setup, mocking the UI Kit + SDK, testing custom bubbles / headers / composer actions, snapshot pitfalls, E2E with Detox vs Maestro, and CI integration. Covers what to test vs what to skip."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5; Jest ^29; @testing-library/react-native ^12"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory"
metadata:
author: "CometChat"
version: "1.0.0"
tags: "cometchat react-native testing jest rnt detox maestro ci"
---
## Purpose
Teaches Claude how to write and run tests against a CometChat React Native integration. Covers:
- Unit / component tests with Jest + React Native Testing Library (RNTL)
- How to mock `@cometchat/chat-uikit-react-native` and `@cometchat/chat-sdk-react-native` (both pull in native modules that fail in Node's jest-expo / jest-react-native environments)
- Testing custom bubbles, headers, composer actions, empty states
- Snapshot testing pitfalls specific to theme-driven components
- E2E with Detox (iOS + Android native drivers) vs Maestro (declarative YAML)
- Which tests catch real regressions vs which are flaky churn
Ground truth: `@cometchat/chat-uikit-react-native@5.3.3`'s example jest config (`examples/SampleAppWithPushNotifications/jest.config.js`) and the standard RN testing toolkit docs (callstack.github.io/react-native-testing-library, wix.github.io/Detox, maestro.mobile.dev).
---
## 1. What to test, what to skip
Not every test is worth the maintenance cost. A few rules of thumb:
**Worth testing:**
- Custom components you wrote (custom bubble, custom header, empty-state view)
- Navigation logic triggered by CometChat events (push tap → deep-link)
- Message render logic with text formatters
- Your provider chain wires correctly (four-wrapper order, init called, login called)
- Production auth token refresh + retry logic
- User-ID mapping (Firebase UID → CometChat UID)
**Skip:**
- UI Kit internals — that's the UI Kit's responsibility. Testing `<CometChatConversations>` renders a list is testing CometChat's code.
- Realtime delivery (A sends, B receives) — requires real servers; flaky and slow; use manual QA or E2E with real accounts.
- Presence / typing indicators — race-prone, depend on socket state.
- Snapshot tests of CometChat components — theme changes, UI Kit updates, and `cometchat-native-theming` edits all churn the snapshots with no real signal.
- Native module calls (camera, picker) — Jest's mocks already return stubs; testing them verifies the mock, not the integration.
The golden rule: if the test fails because **your code** changed, it's valuable. If it fails because **the UI Kit updated** or **a network blip happened**, it's churn.
---
## 2. Toolchain
| Layer | Tool | Why |
|---|---|---|
| Unit + component tests | Jest + `@testing-library/react-native` | The RN default. Preset handles metro module resolution. |
| Mocking | Jest `moduleNameMapper` + manual mocks | UI Kit imports native modules — can't run real components in a Node env. |
| Snapshot | Jest's built-in | Use sparingly — see §7 |
| E2E | Maestro OR Detox | See §10 for tradeoff |
| CI | GitHub Actions / EAS / Bitrise | §11 |
Install:
```bash
# Bare RN
npm install --save-dev jest @testing-library/react-native @testing-library/jest-native \
react-test-renderer @types/jest
# Expo
npx expo install --dev jest-expo @testing-library/react-native @testing-library/jest-native \
react-test-renderer
```
`jest-expo` wraps `react-native` preset with Expo-specific module resolution (handles `expo-modules-core`, `expo-router`, etc.).
---
## 3. Jest config
**Bare RN** — `jest.config.js`:
```js
module.exports = {
preset: "react-native",
setupFilesAfterEach: ["<rootDir>/jest.setup.ts"],
transformIgnorePatterns: [
"node_modules/(?!(?:react-native|@react-native|@react-navigation|" +
"@cometchat/chat-uikit-react-native|@cometchat/chat-sdk-react-native|" +
"react-native-.+|@notifee/react-native)/)",
],
moduleNameMapper: {
"^@cometchat/chat-uikit-react-native$": "<rootDir>/__mocks__/cometchat-uikit.ts",
"^@cometchat/chat-sdk-react-native$": "<rootDir>/__mocks__/cometchat-sdk.ts",
},
};
```
**Expo** — `jest.config.js`:
```js
module.exports = {
preset: "jest-expo",
setupFilesAfterEach: ["<rootDir>/jest.setup.ts"],
transformIgnorePatterns: [
"node_modules/(?!(?:(jest-)?react-native|@react-native|expo(nent)?|@expo(nent)?/.*|" +
"@expo-google-fonts/.*|react-navigation|@react-navigation/.*|" +
"@cometchat/chat-uikit-react-native|@cometchat/chat-sdk-react-native|" +
"@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)/)",
],
moduleNameMapper: {
"^@cometchat/chat-uikit-react-native$": "<rootDir>/__mocks__/cometchat-uikit.ts",
"^@cometchat/chat-sdk-react-native$": "<rootDir>/__mocks__/cometchat-sdk.ts",
},
};
```
**`transformIgnorePatterns` matters.** By default Jest doesn't transform anything under `node_modules`, but CometChat ships ES module source. Without the pattern, Jest errors with `SyntaxError: Unexpected token 'export'`. The UI Kit + SDK names must be in the allow list.
---
## 4. Global setup — `jest.setup.ts`
```ts
import "@testing-library/jest-native/extend-expect";
// Silence RN's "AnimatedValue" warning noise in tests
jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
// Mock native modules that the UI Kit pulls in
jest.mock("react-native-gesture-handler", () => {
const View = require("react-native/Libraries/Components/View/View");
return {
GestureHandlerRootView: View,
PanGestureHandler: View,
TapGestureHandler: View,
State: {},
Directions: {},
};
});
jest.mock("react-native-reanimated", () =>
require("react-native-reanimated/mock"),
);
jest.mock("react-native-safe-area-context", () => ({
SafeAreaProvider: ({ children }: { children: React.ReactNode }) => children,
SafeAreaView: ({ children }: { children: React.ReactNode }) => children,
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
}));
// Silence console.warn from legacy components in tests — re-enable locally if debugging
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => {
if (
typeof args[0] === "string" &&
/componentWill|Unable to find|act\(\)/i.test(args[0])
) {
return;
}
originalWarn(...args);
};
```
---
## 5. Mocking the UI Kit
The UI Kit's top-level components (`CometChatConversations`, `CometChatMessageList`, etc.) wire socket listeners, call native modules, and render FlatLists with async data. Rendering them in Jest is more effort than value.
**Strategy: mock them as transparent Views that forward children.** This lets your tests verify your integration (are the right props being passed? does the right component mount in the right screen?) without pulling in the real implementation.
`__mocks__/cometchat-uikit.ts`:
```ts
import React from "react";
import { View } from "react-native";
const passThrough = (name: string) =>
React.forwardRef<unknown, Record<string, unknown>>((props, ref) => {
const { children, ...rest } = props as { children?: React.ReactNode };
return (
<View ref={ref as never} testID={name} {...rest}>
{children}
</View>
);
});
export const CometChatUIKit = {
init: jest.fn(async () => undefined),
login: jest.fn(async () => ({ getUid: () => "cometchat-uid-1" })),
logout: jest.fn(async () => undefined),
getLoggedinUser: jest.fn(async () => ({ getUid: () => "cometchat-uid-1" })),
};
export const UIKitSettingsBuilder = class {
setAppId() { return this; }
setRegion() { return this; }
setAuthKey() { return this; }
subscribePresenceForAllUsers() { return this; }
build() { return {}; }
};
export const CometChatThemeProvider = passThrough("CometChatThemeProvider");
export const CometChatI18nProvider = passThrough("CometChatI18nProvider");
export const CometChatConversations = passThrough("CometChatConversations");
export const CometChatMessageList = passThrough("CometChatMessageList");
export const CometChatMessageComposer = passThrough("CometChatMessageComposer");
export const CometChatMessageHeader = passThrough("CometChatMessageHeader");
export const CometChatUsers = passThrough("CometChatUsers");
export const CometChatGroups = passThrough("CometChatGroups");
export const CometChatIncomingCall = passThrough("CometChatIncomingCall");
export const CometChatOutgoingCall = passThrough("CometChatOutgoingCall");
export const CometChatUIEventHandler = {
addUIListener: jest.fn(),
removeListener: jest.fn(),
};
export const CometChatUIEvents = {};
export const useTheme = () => ({
color: {
primary: "#6852D6",
background1: "#FFFFFF",
textPrimary: "#141414",
},
typography: {
heading1: { fontFamily: "System", fontSize: 28 },
body1: { fontFamily: "System", fontSize: 16 },
},
});
```
`__mocks__/cometchat-sdk.ts`:
```ts
export const CometChat = {
getUser: jest.fn(async (uid: string) => ({ getUid: () => uid, getName: () => "Test User" })),
getGroup: jest.fn(async (guid: string) => ({ getGuid: () => guid, getName: () => "Test Group" })),
addMessageListener: jest.fn(),
removeMessageListener: jest.fn(),
};
export const CometChatNotifications = {
PushPlatforms: {
FCM_REACT_NATIVE_ANDROID: "fcm-android",
FCM_REACT_NATIVE_IOS: "fcm-ios",
APNS_REACT_NATIVE_DEVICE: "apns-device",
APNS_REACT_NATIVE_VOIP: "apns-voip",
},
registerPushToken: jest.fn(async () => ({ success: true })),
unregisterPushToken: jest.fn(async () => ({ success: true })),
};
```
Every real `<CometChatMessageList>` in your code renders as `<View testID="CometChatMessageList">` in tests. You can assert on `testID` + the props you passed.
---
## 6. Testing a custom component
Example — a custom chat screen that renders `<CometChatMessageList>` for a specific user:
```tsx
// src/screens/MessagesScreen.tsx
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { CometChatMessageList } from "@cometchat/chat-uikit-react-native";
import { useEffect, useState } from "react";
export function MessagesScreen({ uid }: { uid: string }) {
const [user, setUser] = useState<CometChat.User | null>(null);
useEffect(() => {
CometChat.getUser(uid).then(setUser);
}, [uid]);
if (!user) return null;
return <CometChatMessageList user={user} hideReplyInThreadOption />;
}
```
Test:
```tsx
// src/screens/__tests__/MessagesScreen.test.tsx
import { render, waitFor } from "@testing-library/react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { MessagesScreen } from "../MessagesScreen";
test("fetches user then renders MessageList", async () => {
const { getByTestId, queryByTestId } = render(<MessagesScreen uid="alice" />);
// Before fetch resolves — nothing rendered
expect(queryByTestId("CometChatMessageList")).toBeNull();
// After fetch resolves — list renders with user prop
await waitFor(() => expect(getByTestId("CometChatMessageList")).toBeTruthy());
expect(CometChat.getUser).toHaveBeenCalledWith("alice");
});
test("passes hideReplyInThreadOption to MessageList", async () => {
const { findByTestId } = render(<MessagesScreen uid="alice" />);
const list = await findByTestId("CometChatMessageList");
// The mocked component stored props on the View — check them
expect(list.props.hideReplyInThreadOption).toBe(true);
});
```
The second test is the valuable one — it guards the mandatory `hideReplyInThreadOption` flag (hard rule §4 in `cometchat-native-core`) against a future refactor dropping it.
---
## 7. Snapshot testing — use sparingly
**Do snapshot:**
- Pure presentational components with no UI Kit dependency
- Custom bubble renderers with fixed inputs
- Data transforms (message → display string)
**Don't snapshot:**
- Anything wrapped in `CometChatThemeProvider` — a token change churns snapshots with no regression meaning.
- Components rendering UI Kit internals — even with mocks, prop churn from UI Kit updates churns your snapshots.
- Navigators / full screens — too many variables.
```tsx
// Good — isolated, theme-free
test("formatTimestamp(1700000000000) matches snapshot", () => {
expect(formatTimestamp(1_700_000_000_000)).toMatchInlineSnapshot(`"Tue, 14 Nov 2023"`);
});
```
If a snapshot test churns on every UI Kit update, delete it — it's net-negative.
---
## 8. Testing the provider chain
The four-wrapper chain (hard rule §3 in `cometchat-native-core`) is one of the most common regressions AI edits introduce. Test that all four wrappers render:
```tsx
// src/App.test.tsx
import { render } from "@testing-library/react-native";
import App from "./App";
test("App mounts all four CometChat wrappers", () => {
const { getByTestId } = render(<App />);
// The mocked wrappers each render a View with testID matching their name
expect(getByTestId("CometChatThemeProvider")).toBeTruthy();
// Note: GestureHandlerRootView and SafeAreaProvider are pass-through Views
// without distinct testIDs in our setup, so assert via presence of children
// OR extend the mock in jest.setup.ts to add testIDs.
});
```
For the `GestureHandlerRootView` + `SafeAreaProvider` assertion, extend their mocks in `jest.setup.ts` to add `testID`:
```ts
jest.mock("react-native-gesture-handler", () => {
const { View } = require("react-native");
return {
GestureHandlerRootView: (props: any) =>
require("react").createElement(View, { ...props, testID: "GestureHandlerRootView" }),
// ...
};
});
```
---
## 9. Testing login lifecycle
The `ensureLoggedIn` helper (hard rule §2 in `cometchat-native-core`) must handle concurrent calls safely:
```tsx
// src/providers/__tests__/CometChatProvider.test.tsx
import { CometChatUIKit } from "@cometchat/chat-uikit-react-native";
import { ensureLoggedIn } from "../CometChatProvider";
beforeEach(() => {
jest.clearAllMocks();
});
test("concurrent ensureLoggedIn calls only invoke login once", async () => {
(CometChatUIKit.getLoggedinUser as jest.Mock).mockResolvedValue(null);
const results = await Promise.all([
ensureLoggedIn("alice"),
ensureLoggedIn("alice"),
ensureLoggedIn("alice"),
]);
expect(CometChatUIKit.login).toHaveBeenCalledTimes(1);
});
test("already-logged-in skips login entirely", async () => {
(CometChatUIKit.getLoggedinUser as jest.Mock).mockResolvedValue({
getUid: () => "alice",
});
await ensureLoggedIn("alice");
expect(CometChatUIKit.login).not.toHaveBeenCalled();
});
```
These two tests catch the most common `ensureLoggedIn` breakages — dropping the module-level promise guard, or forgetting the `getLoggedinUser` short-circuit.
---
## 10. E2E — Detox vs Maestro
Two choices for end-to-end. Different philosophies.
| | Detox | Maestro |
|---|---|---|
| Config | Native drivers (iOS + Android). `.detoxrc.js`. | YAML flows. Single binary. |
| Language | JavaScript / TypeScript | YAML |
| Setup | Heavy — Xcode build, Detox CLI, Jest runner | Light — brew install, run CLI |
| CI | Slow (full native build each run) | Fast (reuses install) |
| Speed | Flaky in CI, reliable locally | Fast, stable |
| iOS + Android parity | Yes | Yes |
| Cloud runs | No native cloud support | Maestro Cloud (paid) |
| Learning curve | Steep if you don't know RN internals | Low |
**Recommendation: Maestro for most teams.** Flows are readable, runs in seconds, CI-friendly. Detox makes sense if you have existing Jest infrastructure and want E2E to live in the same runner.
### Maestro flow (recommended)
`.maestro/chat-happy-path.yaml`:
```yaml
appId: com.yourapp.mobile
---
- launchApp
- tapOn: "Login"
- inputText: "cometchat-uid-1"
- tapOn: "Continue"
- assertVisible: "Messages"
- tapOn: "Messages"
- assertVisible: "Conversations"
- tapOn: id: "conversation-cometchat-uid-2"
- inputText: "Hello from Maestro"
- tapOn: id: "send-button"
- assertVisible: "Hello from Maestro"
```
Run:
```bash
maestro test .maestro/chat-happy-path.yaml
```
Needs your RN `<CometChatMessageComposer>` to expose `testID="send-button"` — the UI Kit supports this via the `sendButtonStyle` slot or via a Custom view template.
### Detox
`.detoxrc.js` (abbreviated):
```js
module.exports = {
testRunner: { args: { $0: "jest", config: "e2e/jest.config.js" } },
apps: {
"ios.debug": {
type: "ios.app",
binaryPath: "ios/build/Build/Products/Debug-iphonesimulator/YourApp.app",
},
},
devices: { simulator: { type: "ios.simulator", device: { type: "iPhone 15" } } },
configurations: {
"ios.sim.debug": { device: "simulator", app: "ios.debug" },
},
};
```
Test:
```ts
// e2e/chat.test.ts
describe("chat flow", () => {
beforeAll(async () => {
await device.launchApp();
});
it("sends a message", async () => {
await element(by.text("Login")).tap();
await element(by.id("uid-input")).typeText("cometchat-uid-1");
await element(by.text("Continue")).tap();
await element(by.text("Messages")).tap();
await element(by.id("conversation-cometchat-uid-2")).tap();
await element(by.id("message-input")).typeText("Hello from Detox");
await element(by.id("send-button")).tap();
await expect(element(by.text("Hello from Detox"))).toBeVisible();
});
});
```
Detox needs a native dev build first (`detox build --configuration ios.sim.debug`) — slow in CI.
### What NOT to E2E
- Login with a real auth provider (Firebase / Clerk). Mock the auth callback or use a test account with a fixed password.
- Real push delivery. Fire via a CI-only fake push tool, or skip entirely.
- Group calls with real peers. Use two simulators only if Detox/Maestro supports it (both do, but flaky).
---
## 11. CI integration
### GitHub Actions — Jest on every push
`.github/workflows/test.yml`:
```yaml
name: test
on: [push, pull_request]
jobs:
jest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx tsc --noEmit
- run: npm test -- --ci --coverage
```
### Maestro in CI
Maestro runs on macOS runners (iOS) or Linux runners with Android emulators. The `mobile-dev-inc/action-maestro-cloud` action simplifies it:
```yaml
e2e:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Build iOS
run: |
cd ios
pod install
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp \
-sdk iphonesimulator -configuration Debug \
-derivedDataPath build
- name: Run Maestro flows
uses: mobile-dev-inc/action-maestro-cloud@v1
with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
app-file: ios/build/Build/Products/Debug-iphonesimulator/YourApp.app
workspace: .maestro
```
### EAS + Expo
If you're on EAS, `eas build --profile preview` followed by `maestro test` against the preview build works for CI smoke tests. EAS Test (paid) orchestrates Maestro runs across multiple devices.
---
## 12. Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| `SyntaxError: Unexpected token 'export'` | Jest not transforming a UI Kit or SDK file | Add package name to `transformIgnorePatterns` allow list |
| `TypeError: Cannot read properties of undefined (reading 'Directions')` | Gesture handler native module missing | Mock in `jest.setup.ts` (see §4) |
| Tests hang for 30s+ | Real WebSocket or fetch not mocked | Add `jest.useFakeTimers()` + mock `fetch` |
| Snapshot fails after no code change | Theme token update churned output | Either delete the snapshot (§7) or run `--updateSnapshot` |
| `useInsertionEffect must not schedule updates` warning | React Navigation dev warning, harmless in tests | Silence in `jest.setup.ts` (see §4) |
| `Could not find React Testing Library matchers` | `@testing-library/jest-native` not extended | `import "@testing-library/jest-native/extend-expect"` in setup |
| Maestro "app not installed" | Bundle ID mismatch or simulator not booted | `xcrun simctl boot "iPhone 15"`, verify `appId` in YAML |
| Detox "Cannot find element" | `testID` not set on UI Kit component | Add via slot view template or custom view; don't rely on text matching |
---
## 13. Hard rules
- **Mock the UI Kit and SDK in every test file.** Running real components in Node fails on native modules and wastes CI time even when it works.
- **Don't test what the UI Kit already tests.** You're responsible for YOUR code — bubbles, headers, navigation, auth mapping. UI Kit internals are CometChat's job.
- **Skip realtime and presence.** They require real servers and produce flaky suites. Use manual QA or E2E with real test accounts.
- **Assert on `testID` and prop values, not on pixel output.** Theme changes, font metrics, and platform differences all churn pixel-level assertions.
- **Keep snapshot tests scoped.** Use for pure data transforms and isolated presentational code. Never snapshot a full screen.
- **E2E tests run against a dev build, not Jest.** Don't try to test real CometChat flow in Jest — it belongs in Detox/Maestro.
---
## 14. Skill routing
| This skill | Covers |
|---|---|
| `cometchat-native-testing` (this) | Jest + RNTL setup, mocking UI Kit + SDK, component / provider / login tests, Detox vs Maestro for E2E, CI |
| `cometchat-native-core` | The provider chain + login concurrency patterns you're testing |
| `cometchat-native-components` | Component catalog — what props to assert in tests |
| `cometchat-native-customization` | DataSource decorators + custom views — test per §6 |
| `cometchat-native-push` | Push tests (mock `CometChatNotifications`); E2E tap-to-deep-link needs a real device |
| `cometchat-native-troubleshooting` | Metro cache / pod install / native module errors (often surface first in a CI run) |
---
name: cometchat-native-theming
description: "CometChatThemeProvider + CometChatI18nProvider — color tokens, typography, dark mode, per-component style overrides, and localization (18 built-in languages + custom translations). The JS theme object replaces CSS variables."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native theming colors dark-mode typography"
---
## Purpose
Teaches Claude how to theme and localize the React Native UI Kit via `CometChatThemeProvider` + `CometChatI18nProvider`. No CSS — React Native uses a JS theme object instead. This skill covers color tokens, typography, light/dark modes, per-component style overrides, the `useTheme()` hook for custom views, and localization (18 built-in languages, device auto-detect, custom translation overrides) via `useCometChatTranslation()`.
**Read `cometchat-native-core` first** (the wrapper chain that includes `CometChatThemeProvider`) before this skill. `cometchat-native-components` § 13 covers per-component `style={}` overrides, which are a sibling concern to theming.
Ground truth: `docs/ui-kit/react-native/theme.mdx`, `colors.mdx`, `component-styling.mdx`, `message-bubble-styling.mdx`, and `packages/ChatUiKit/src/theme/type.ts` (the canonical type definitions).
---
## 1. How theming works (no CSS — JS theme object)
React Native has no CSS. Instead:
```
CometChatThemeProvider
↓ (provides theme via React Context)
every <CometChat*> component reads theme via internal useTheme()
component's default styles merge with theme overrides → rendered styles
```
The theme object you pass has two top-level keys for light/dark variants:
```tsx
<CometChatThemeProvider
theme={{
mode: "light", // or "dark", or omit for OS-default
light: { color: { primary: "#F76808" } },
dark: { color: { primary: "#FF8A3D" } },
}}
>
<App />
</CometChatThemeProvider>
```
### Style precedence (highest to lowest)
1. **Component `style={}` prop** — wins always. Per-component tweak, overrides everything.
2. **Custom theme** via `CometChatThemeProvider` — app-wide.
3. **Default theme** — the UI Kit's built-in palette.
So for a one-off color on a single component, use `style={}`. For a brand-wide change (primary color everywhere), use the theme.
### Deep merge
Theme values are deeply merged with defaults — you only specify what you want to change:
```tsx
theme={{
light: {
color: {
primary: "#F76808", // override just primary; everything else keeps defaults
},
typography: {
heading1: { fontWeight: "700" }, // override just heading1 weight
},
},
}}
```
---
## 2. The CometChatThemeProvider
### Minimum setup — follow system light/dark
```tsx
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
<CometChatThemeProvider>
{/* children read the current system mode automatically */}
</CometChatThemeProvider>
```
### Force a mode
```tsx
<CometChatThemeProvider theme={{ mode: "light" }}>{/* ... */}</CometChatThemeProvider>
<CometChatThemeProvider theme={{ mode: "dark" }}>{/* ... */}</CometChatThemeProvider>
```
### Placement in the wrapper chain
`CometChatThemeProvider` is one of the four required wrappers — goes right above `CometChatProvider`, below `SafeAreaProvider` (see `cometchat-native-core` § 3):
```tsx
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider theme={/* your theme */}>
<CometChatProvider appId={...} region={...} authKey={...}>
<YourApp />
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
```
Without `CometChatThemeProvider`, components throw or fall back to minimal styles. Even if you don't customize anything, the wrapper is mandatory.
---
## 3. Color tokens
Every color is a hex string (`"#F76808"` — never `"rgb(...)"` or named colors).
### Primary (brand accent)
| Token | Controls |
|---|---|
| `primary` | Outgoing message bubbles, send button, active tabs, buttons |
| `extendedPrimary50–900` | Auto-derived shades of primary. Used for hover, pressed, subtle accents. **Only override these if you need finer control** — the auto-derivation is usually correct. |
### Neutrals (surfaces + borders)
| Token | Default (light) | Controls |
|---|---|---|
| `neutral50` | `#FFFFFF` | White/light surface, background1 default |
| `neutral100` | `#FAFAFA` | background2 default |
| `neutral200` | `#F5F5F5` | background3 default |
| `neutral300` | `#E8E8E8` | Incoming bubble default, borders |
| `neutral400` | `#DCDCDC` | Divider lines |
| `neutral500` | `#A1A1A1` | Placeholder / muted text, iconSecondary default |
| `neutral600` | `#727272` | textSecondary (timestamps, subtitles) |
| `neutral700` | `#5B5B5B` | Body text tier 3 |
| `neutral800` | `#434343` | Headings default |
| `neutral900` | `#141414` | textPrimary default, iconPrimary default |
### Background aliases
| Token | Maps to (default) | Controls |
|---|---|---|
| `background1` | `neutral50` | Main app background |
| `background2` | `neutral100` | Sidebars, panels |
| `background3` | `neutral200` | Nested panels, cards |
| `background4` | `neutral300` | Additional surface |
### Text
| Token | Default | Controls |
|---|---|---|
| `textPrimary` | `neutral900` | Main body text |
| `textSecondary` | `neutral600` | Timestamps, subtitles |
| `textTertiary` | `neutral500` | Hints, placeholders |
| `textHighlight` | `primary` | Links, mentions |
### Icon
| Token | Default | Controls |
|---|---|---|
| `iconPrimary` | `neutral900` | Active / default icons |
| `iconSecondary` | `neutral500` | Inactive icons |
| `iconHighlight` | `primary` | Action icons |
### Semantic (state indicators)
| Token | Default (light) | Controls |
|---|---|---|
| `info` | `#0B7BEA` | Info callouts, links |
| `warning` | `#FFAB00` | Warning callouts |
| `success` | `#09C26F` | Online indicator, success messages |
| `error` | `#F44649` | Error messages, validation |
### Bubble-specific
| Token | Default | Controls |
|---|---|---|
| `sendBubbleBackground` | `primary` | Outgoing bubble bg |
| `sendBubbleText` | `staticWhite` (`#FFFFFF`) | Outgoing bubble text |
| `receiveBubbleBackground` | `neutral300` | Incoming bubble bg |
| `receiveBubbleText` | `neutral900` | Incoming bubble text |
### Static (never flip light/dark)
| Token | Value | Controls |
|---|---|---|
| `staticBlack` | `#141414` | Fixed dark elements (overlays, opacity-based) |
| `staticWhite` | `#FFFFFF` | Fixed light elements |
---
## 4. Mode: light / dark / system
### Follow system
Don't pass `mode` — the provider reads the OS setting via `useColorScheme()` and re-renders on change. The user gets automatic dark mode when they flip the system setting.
```tsx
<CometChatThemeProvider>{/* ... */}</CometChatThemeProvider>
```
### Force a specific mode
```tsx
<CometChatThemeProvider theme={{ mode: "dark" }}>{/* ... */}</CometChatThemeProvider>
```
### Toggle controlled by your app
If your app has its own dark-mode switch (stored in user prefs or Redux), drive `mode` from that state:
```tsx
const [darkMode, setDarkMode] = useState(false);
// ...
<CometChatThemeProvider theme={{ mode: darkMode ? "dark" : "light" }}>
<Switch value={darkMode} onValueChange={setDarkMode} />
<App />
</CometChatThemeProvider>
```
The provider re-renders children and they pick up the new theme immediately.
### Dark-mode palette
Override the `dark` branch of the theme for a custom dark palette:
```tsx
<CometChatThemeProvider
theme={{
light: { color: { primary: "#6852D6" } },
dark: { color: { primary: "#A594F3", background1: "#0B0B0F" } },
}}
>
```
---
## 5. Typography overrides
The theme has a `typography` block with tokens per role:
```tsx
<CometChatThemeProvider
theme={{
light: {
typography: {
heading1: { fontFamily: "Inter-Bold", fontSize: 28, fontWeight: "700" },
heading2: { fontFamily: "Inter-SemiBold", fontSize: 20 },
body1: { fontFamily: "Inter-Regular", fontSize: 15 },
caption1: { fontFamily: "Inter-Regular", fontSize: 12 },
// ... etc
},
},
}}
>
```
Common tokens: `heading1`, `heading2`, `heading3`, `heading4`, `body1`, `body2`, `caption1`, `caption2`, `button1`, `button2`. Each follows the RN `TextStyle` shape — `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`.
### Custom font setup
React Native font loading is NOT covered by the UI Kit — use your project's existing font system:
- **Expo**: `useFonts()` from `expo-font`, load before rendering the provider
- **Bare RN**: add fonts to `ios/<App>/Info.plist` `UIAppFonts` + `android/app/src/main/assets/fonts/` + run `npx react-native-asset`
Only reference a `fontFamily` in the theme once the font is actually loaded — otherwise iOS shows the system default and Android crashes.
---
## 6. Per-component style blocks
Beyond color / typography, the theme has per-component style blocks for fine control. These sit inside the `light` / `dark` branches:
```tsx
<CometChatThemeProvider
theme={{
light: {
// component-specific overrides
conversationStyles: {
containerStyle: { backgroundColor: "#FAFAFA" },
},
messageHeaderStyles: {
titleStyle: { fontSize: 18 },
},
messageListStyles: {
containerStyle: { padding: 8 },
sendBubbleStyle: {
backgroundColor: "#F76808",
textStyle: { color: "#FFFFFF" },
},
receiveBubbleStyle: {
backgroundColor: "#F5F5F5",
textStyle: { color: "#141414" },
},
},
messageComposerStyles: {
containerStyle: { backgroundColor: "#FFF", borderTopWidth: 1, borderTopColor: "#E8E8E8" },
},
},
}}
>
```
Common component-style keys: `conversationStyles`, `usersStyles`, `groupsStyles`, `groupMembersStyles`, `messageHeaderStyles`, `messageListStyles`, `messageComposerStyles`, `threadHeaderStyles`, `callButtonsStyles`, `callLogsStyles`.
Each block has the same nested shape as the component's `style` prop (see `cometchat-native-components` § 13).
### Source of truth for available keys
The exact list of style keys per component is authoritative in the kit's type file:
```
packages/ChatUiKit/src/theme/type.ts
```
If you're overriding a component style and the TypeScript compiler complains about an unknown key, check that file (or use `useTheme()` + autocomplete in your IDE).
---
## 7. Common recipes
### Match a brand color (most common)
```tsx
<CometChatThemeProvider
theme={{ light: { color: { primary: "#FF6B35" } } }}
>
<App />
</CometChatThemeProvider>
```
This single line changes the outgoing message bubble color, send button color, active tab indicator, and every primary accent in the UI. The `extendedPrimary50–900` tints are auto-derived from `primary`.
### Dark mode + custom brand
```tsx
<CometChatThemeProvider
theme={{
light: { color: { primary: "#FF6B35" } },
dark: { color: { primary: "#FF8F66", background1: "#1A1A1A" } },
}}
>
<App />
</CometChatThemeProvider>
```
### Custom message-bubble colors
```tsx
<CometChatThemeProvider
theme={{
light: {
color: {
sendBubbleBackground: "#FF6B35",
sendBubbleText: "#FFFFFF",
receiveBubbleBackground: "#F0F0F0",
receiveBubbleText: "#1A1A1A",
},
},
}}
>
```
Overriding the bubble tokens directly is cleaner than doing it via `messageListStyles.sendBubbleStyle` — the tokens apply consistently everywhere bubbles render (main list + thread panel + search results).
### Custom font across the whole UI
1. Load font (Expo `useFonts` or bare `npx react-native-asset`)
2. Override the typography block:
```tsx
<CometChatThemeProvider
theme={{
light: {
typography: {
heading1: { fontFamily: "Inter-Bold" },
heading2: { fontFamily: "Inter-SemiBold" },
heading3: { fontFamily: "Inter-SemiBold" },
heading4: { fontFamily: "Inter-Medium" },
body1: { fontFamily: "Inter-Regular" },
body2: { fontFamily: "Inter-Regular" },
caption1: { fontFamily: "Inter-Regular" },
caption2: { fontFamily: "Inter-Regular" },
button1: { fontFamily: "Inter-SemiBold" },
button2: { fontFamily: "Inter-Medium" },
},
},
}}
>
```
---
## 8. Reading the theme in custom views
When you write a custom slot view (e.g. a `TitleView` on `CometChatMessageHeader`) and want your custom component to match the theme, use the `useTheme()` hook:
```tsx
import { useTheme } from "@cometchat/chat-uikit-react-native";
function CustomTitle({ user }: any) {
const theme = useTheme();
return (
<Text style={{
color: theme.color.textPrimary,
fontFamily: theme.typography.heading3.fontFamily,
fontSize: theme.typography.heading3.fontSize,
}}>
{user.getName()}
</Text>
);
}
// Wire into a header:
<CometChatMessageHeader user={selectedUser} TitleView={(user) => <CustomTitle user={user} />} />
```
This is how you write custom views that automatically follow dark mode — by reading tokens from `useTheme()` instead of hardcoding colors.
---
## 9. Localization — `CometChatI18nProvider`
Theming and localization are separate concerns but ship together. If your users aren't all English-speaking, wire `CometChatI18nProvider` alongside `CometChatThemeProvider`. Every string rendered by the UI Kit (message-action labels, empty states, system messages, alerts) flows through the i18n layer.
### 9a. Built-in locales
The UI Kit ships translations for 18 languages out of the box:
```
de, en, es, fr, hi, hu, it, ja, ko, lt, ms, nl, pt, ru, sv, tr, zh, zh-tw
```
### 9b. Wrapper chain with i18n — five wrappers, not four
`CometChatI18nProvider` goes above `CometChatThemeProvider` (theme is a child of i18n, not the other way around):
```tsx
import { CometChatI18nProvider, CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatI18nProvider>
<CometChatThemeProvider>
<CometChatProvider>
<YourApp />
</CometChatProvider>
</CometChatThemeProvider>
</CometChatI18nProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
```
### 9c. Auto-detect (default)
With no props, `CometChatI18nProvider` reads the device locale via `react-native-localize` and picks the matching language if available, falling back to English. No setup needed beyond the wrapper.
**Install `react-native-localize`** (required peer dep for auto-detect):
```bash
npx expo install react-native-localize # Expo managed
# or
npm install react-native-localize && cd ios && pod install && cd .. # bare RN
```
### 9d. Force a specific language
Override the device default via `selectedLanguage`:
```tsx
<CometChatI18nProvider selectedLanguage="ja">
```
If the user's app has its own language preference (stored in settings / Redux / MMKV), drive `selectedLanguage` from that state. The provider re-renders children on change so strings update immediately.
### 9e. Fallback behavior
Chain: **`selectedLanguage` (if set + available) → device language (if `autoDetectLanguage=true`) → `fallbackLanguage` (default `'en'`)**.
```tsx
<CometChatI18nProvider
selectedLanguage={user.preferredLanguage} // from your app state
autoDetectLanguage={true} // fall back to device language
fallbackLanguage="en" // final fallback
>
```
If the user's preferred language isn't bundled AND there's no custom translation for it, the provider logs a warning and uses the fallback.
### 9f. Custom translations — override or add languages
Pass a `translations` object to override specific keys in an existing locale, or add a brand-new locale the UI Kit doesn't ship:
```tsx
const translations = {
en: {
// Override a built-in English string
"NO_MESSAGES_YET": "Say hello to start the conversation!",
"SENT": "Delivered",
},
th: {
// Add a new language — Thai
"NO_MESSAGES_YET": "ยังไม่มีข้อความ",
"SENT": "ส่งแล้ว",
// ...provide the full key set
},
};
<CometChatI18nProvider selectedLanguage="th" translations={translations}>
```
The translation schema is a flat `{ KEY: "string" }` map. Keys are screaming-snake-case (`NO_MESSAGES_YET`, `MESSAGE_COMPOSER_MENTION_ALL`, `TRANSLATE`, etc.). Full key list lives at `packages/ChatUiKit/src/shared/resources/CometChatLocalizeNew/resources/en/translation.json` in the UI Kit source — grep for `"KEY":` there to find the exact key for a string you want to override.
### 9g. Reading the language inside custom views
When you write a custom slot view and need the current language (or want to translate your own strings using the same key set), use the `useCometChatTranslation` hook:
```tsx
import { useCometChatTranslation } from "@cometchat/chat-uikit-react-native";
function CustomEmptyState() {
const { t, language } = useCometChatTranslation();
return <Text>{t("NO_MESSAGES_YET")}</Text>;
}
```
The hook also exposes `availableLanguages` — useful for building a language-picker UI.
### 9h. Common pitfall — i18n outside the provider
Calling `useCometChatTranslation()` from a component rendered OUTSIDE `CometChatI18nProvider` (common when a custom view mounts at the navigator root instead of inside the chat subtree) logs `"useCometChatTranslation used outside provider, using fallback translations"` and falls through to English. Check your wrapper chain — i18n must wrap every component that reads translations, which is the whole app tree in practice.
---
## 10. Anti-patterns
1. **Don't pass non-hex colors.** `"rgb(...)"`, `"rgba(...)"`, named colors, or `hsl(...)` will break the kit's internal color math (used to derive `extendedPrimary`). Use `"#RRGGBB"` or `"#RRGGBBAA"` (opacity via alpha).
2. **Don't override `staticBlack` / `staticWhite`.** They're "static" for a reason — used in places where a specific absolute color is needed regardless of theme (overlays, badges on fixed-color avatars). Overriding them breaks visual consistency.
3. **Don't override extended primary colors unless you need to.** `extendedPrimary50–900` are auto-derived from `primary`. Override them only if the auto-derivation doesn't match your brand's tints — and then override the full range, not just one level.
4. **Don't wrap `CometChatThemeProvider` inside a screen.** It belongs at the app root, once. Re-wrapping per screen creates hydration-like flashes on navigation and breaks dark-mode switching.
5. **Don't mix theme overrides and per-component `style={}` for the same property.** `style={}` wins — the theme override becomes dead code. Pick one: theme for app-wide, `style={}` for one-offs.
6. **Don't reference an unloaded font in typography.** iOS silently falls back to system default; Android crashes. Gate the provider on font loading:
```tsx
// Expo example
const [fontsLoaded] = useFonts({ "Inter-Bold": require("./assets/Inter-Bold.ttf") });
if (!fontsLoaded) return null;
return (
<CometChatThemeProvider theme={{ light: { typography: { heading1: { fontFamily: "Inter-Bold" } } } }}>
<App />
</CometChatThemeProvider>
);
```
7. **Don't bypass the theme via `useColorScheme()` in a custom view.** Call `useTheme()` from `@cometchat/chat-uikit-react-native` — that gives you the current theme (including any overrides you set). `useColorScheme()` only gives you the raw system mode.
---
## 11. Verifying a theme change
After changing the theme:
1. Hard-reload the Metro bundler (not Fast Refresh — theme context sometimes doesn't update on Fast Refresh)
2. Send a message — check the outgoing bubble color matches `primary` / `sendBubbleBackground`
3. Toggle dark mode on the device (iOS: Settings → Display; Android: Settings → Display → Dark theme)
4. Check that both modes render without reloading the app
If something looks unstyled or crashes:
- Check the color is a hex string (not a name or rgb)
- Check the font (if you overrode typography) is actually loaded
- Check the override key matches the type in `packages/ChatUiKit/src/theme/type.ts`
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Always read first — init/login/provider wrapper chain |
| `cometchat-native-components` | For per-component `style={}` prop (sibling concern to theming) |
| `cometchat-native-placement` | Where CometChat components go |
| `cometchat-native-theming` | This skill — app-wide color/typography/dark mode |
| `cometchat-native-customization` | Custom slot views + `useTheme()` in your own components |
| `cometchat-native-expo-patterns` | Expo font loading via `expo-font` |
| `cometchat-native-bare-patterns` | Bare RN font loading via `react-native-asset` |
| `cometchat-native-troubleshooting` | Colors not applying, dark mode not switching, font shows system default |
---
name: cometchat-native-troubleshooting
description: "Diagnose CometChat React Native UI Kit integration failures — init/login, gesture handler, pod install, iOS privacy manifest, Android Maven, Metro cache, permissions, calls, extensions, v4-to-v5 upgrade. For push-specific symptoms see cometchat-native-push § 12."
license: "MIT"
compatibility: "Node.js >=18; React Native >=0.70; @cometchat/chat-uikit-react-native ^5"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat react-native troubleshooting metro pods permissions upgrade"
---
## Purpose
Teaches Claude how to diagnose and fix CometChat React Native integration failures. Covers every category of failure I've seen across Expo + bare RN, with an up-front triage flow so Claude asks the right questions before assuming a fix.
**Read `cometchat-native-core` first** — most "why doesn't this work" issues trace to the init/login/wrapper chain explained there.
Ground truth: `docs/ui-kit/react-native/troubleshooting.mdx`, `apple-privacy-manifest-guide.mdx`, `upgrading-from-v4.mdx`, and first-hand failure modes from real integrations.
---
## 1. Triage — read project state before guessing
When the user reports a problem, gather facts before proposing a fix. Ask + run these first:
### 1a. Which framework?
```bash
# Expo or bare?
grep -E '"expo":|"expo-router":' package.json
ls -d ios android 2>/dev/null # bare RN has these dirs at root
ls app.json app.config.js 2>/dev/null
```
- `expo` in dependencies + no `ios/android` folders → **Expo managed** → route to `cometchat-native-expo-patterns`
- `ios/` + `android/` folders at root → **bare RN** → route to `cometchat-native-bare-patterns`
- Both (`expo` + `ios/android`) → bare-after-prebuild. Treat as bare.
### 1b. Is the wrapper chain correct?
```bash
# Confirm four wrappers in entry file
grep -E "GestureHandlerRootView|SafeAreaProvider|CometChatThemeProvider|CometChatProvider" \
App.tsx index.js index.ts app/_layout.tsx 2>/dev/null
```
If any are missing, that's almost certainly the cause. See `cometchat-native-core` § 3.
### 1c. Is `react-native-gesture-handler` imported first?
```bash
# Must be line 1 (or 2 with a shebang) of entry
head -5 index.js index.ts App.tsx app/_layout.tsx 2>/dev/null
```
If the import is missing or below React, swipe gestures and bottom sheets silently break — often only in release builds.
### 1d. Is init complete before login?
```bash
grep -A 10 "CometChatUIKit.init" src/providers/CometChatProvider.tsx 2>/dev/null
```
Look for the init-then-login sequence with an `await` or module-level `initialized` flag. Fire-and-forget init is a common cause of "components don't render."
### 1e. Pod install state (iOS)
```bash
ls ios/Pods/ | head -5 2>/dev/null # should list dozens of pods
cat ios/Podfile.lock | head -3 2>/dev/null
```
If `ios/Pods/` is missing or sparse, native modules aren't linked. `cd ios && pod install && cd ..` is the fix.
### 1f. Package deps
```bash
# Check the full UI Kit peer-dep set is installed
for dep in \
@cometchat/chat-sdk-react-native \
@cometchat/chat-uikit-react-native \
react-native-gesture-handler \
react-native-safe-area-context \
react-native-svg \
@react-native-async-storage/async-storage; do
node -e "try{require.resolve('$dep'); console.log('✓ $dep')}catch(e){console.log('✗ MISSING: $dep')}"
done
```
Missing peer deps → install + `pod install` (bare) or `expo install` (Expo) + rebuild.
---
## 2. Symptom → fix lookup tables
Quick-reference tables. Work through in order; if none match, drop into § 3 deep-dives.
### 2a. Initialization + login
| Symptom | Likely cause | Fix |
|---|---|---|
| `CometChatUIKit.init()` fails silently | Invalid `APP_ID` / `REGION` / `AUTH_KEY` | Re-verify from dashboard → your app → Credentials |
| Components render nothing after login | `init()` not awaited before mount | Use the provider pattern from `cometchat-native-core` § 6 with `isReady` gating |
| Blank screen, no errors | Component mounted before init completed | Conditionally render: `if (!isReady) return null` |
| `getLoggedinUser()` returns `null` | Login not called or session expired | Call `CometChatUIKit.login({ uid })` after init resolves |
| Login fails: "UID not found" | User doesn't exist in CometChat | Create via dashboard, SDK, or REST API. For dev, use `cometchat-uid-1` through `cometchat-uid-5` (pre-seeded) |
| "Please wait until the previous login request ends" | `login()` called concurrently (React StrictMode double-mount / navigation remount) | Use the `ensureLoggedIn` module-level promise guard from `cometchat-native-core` § 2 |
| `sendTextMessage()` fails | Not logged in or invalid receiver | Verify `getLoggedinUser()` returns a user + receiver user/group object exists |
| Production build exposes Auth Key | Using `authKey` in production | Switch to server-minted auth tokens via `login({ authToken })`. See `cometchat-native-production` |
### 2b. Gesture handler + wrapper chain
| Symptom | Likely cause | Fix |
|---|---|---|
| Composer swipe gestures not working | `import "react-native-gesture-handler"` not at TOP of entry file | Move to line 1 of `index.js` / `index.ts` / Expo Router's `app/_layout.tsx` |
| BottomSheet doesn't open on swipe | Missing `GestureHandlerRootView` wrapper | Wrap at app root with `<GestureHandlerRootView style={{ flex: 1 }}>` |
| Content overlaps status bar / home indicator | Missing `SafeAreaProvider` | Wrap inside `GestureHandlerRootView` with `<SafeAreaProvider>` |
| Theme tokens return undefined | Missing `CometChatThemeProvider` | Add wrapper. See `cometchat-native-core` § 3 for the full chain |
| "Couldn't find SafeArea context" error | `SafeAreaView` used outside `SafeAreaProvider` | Add `SafeAreaProvider` at root, or use plain `View` inside chat screens |
| Release build: swipe/gesture broken, dev build fine | `react-native-gesture-handler` not line 1 (some bundlers defer it) | Move to line 1 unconditionally |
### 2c. iOS build failures
| Symptom | Likely cause | Fix |
|---|---|---|
| `No such module 'RNGestureHandler'` | Pods not installed | `cd ios && pod install && cd ..` + rebuild |
| `Undefined symbol: _OBJC_CLASS_$_RNCAsyncStorage` | Pods out of sync after `npm install` | `cd ios && pod install && cd ..` |
| `TurboModuleRegistry.getEnforcing(...)` crash at runtime | Native module not linked | `cd ios && pod install && cd ..` + reset Metro cache (`--reset-cache`) |
| App Store rejection `ITMS-91053` | Missing Apple Privacy Manifest | Add `ios/<App>/PrivacyInfo.xcprivacy` with 4 API types. See `cometchat-native-bare-patterns` § 2c |
| Simulator build fails: "arm64 excluded" | Architecture mismatch on Apple Silicon | Add `EXCLUDED_ARCHS[sdk=iphonesimulator*]` post_install in Podfile. See `cometchat-native-features` § 3c |
| Camera/mic permission dialog doesn't appear | Missing `NSCameraUsageDescription` / `NSMicrophoneUsageDescription` | Add to `ios/<App>/Info.plist` (bare) or `app.json` `ios.infoPlist` (Expo) |
| iOS build fails: "deployment target too low" | Calls SDK needs 12+ | Set `IPHONEOS_DEPLOYMENT_TARGET = '12.0'` in Podfile post_install |
| Expo Go crashes: "Main module field cannot be resolved" | Expo Go doesn't support native modules | Use dev builds: `npx expo run:ios` or `eas build --profile development` |
### 2d. Android build failures
| Symptom | Likely cause | Fix |
|---|---|---|
| `Could not find :react-native-async-storage_async-storage:` | Local Maven repo entry missing | Add to `android/build.gradle` `allprojects.repositories` — see `cometchat-native-bare-patterns` § 3b |
| Build crashes on launch | `compileSdkVersion` too low | Set `compileSdkVersion` to 33+ and `minSdkVersion` to 24+ in `android/app/build.gradle` |
| "Permission denied" when picking photos | Missing Android permissions | Add `READ_MEDIA_IMAGES` (API 33+) or `READ_EXTERNAL_STORAGE` (API ≤32) to `AndroidManifest.xml` |
| "Camera permission denied" | Missing permission in manifest or runtime denial | Add `CAMERA` to `AndroidManifest.xml` + verify runtime-grant flow |
| Gradle sync fails after `npm install` | Native module autolinking stale | `cd android && ./gradlew clean && cd ..` then rebuild |
| `Unable to delete file: ...node_modules/...` | Metro lockfile on Windows (if cross-OS) | Restart Metro + IDE |
### 2e. Metro / JS runtime
| Symptom | Likely cause | Fix |
|---|---|---|
| "Unable to resolve module ..." for a dep that IS installed | Metro cache stale | Stop Metro, run with `--reset-cache`: `npx react-native start --reset-cache` or `npx expo start --clear` |
| Fast Refresh doesn't pick up new deps | Native dep change (requires rebuild) | Restart Metro + rebuild (iOS/Android) |
| "Maximum update depth exceeded" after theme change | Theme object recreated each render | Define theme at module scope or in `useMemo(() => ..., [])` |
| App crashes on first JS load | Entry file error (syntax or import order) | Check `index.js` — `react-native-gesture-handler` should be line 1 |
### 2f. Theming
| Symptom | Likely cause | Fix |
|---|---|---|
| Theme overrides don't apply | Missing `CometChatThemeProvider` wrapper | Add at app root. See `cometchat-native-theming` § 2 |
| Dark mode doesn't switch when system toggled | `mode` hardcoded to `"light"` / `"dark"` | Omit the `mode` field to follow system |
| Custom color shows as fallback | Color not a hex string | Use `#RRGGBB` format. `rgb()` / named colors / `hsl()` break the extendedPrimary auto-derivation |
| Custom font shows system default (iOS) / crashes (Android) | Font not loaded before render | Gate the provider on font loading (Expo `useFonts` or bare `react-native-asset`) |
| Icon color not changing via `imageStyle` | Only `tintColor`, `height`, `width` work on default SVG icons | Use `tintColor`. Other props are ignored for built-in icons |
| `useTheme()` returns undefined | Called outside `CometChatThemeProvider` | Ensure the component is a descendant of the provider |
### 2g. Components
| Symptom | Likely cause | Fix |
|---|---|---|
| Slot view (`TitleView`, `LeadingView`, etc.) renders nothing | Slot returned `null` / `undefined` | Slot must return valid JSX |
| Empty message list despite sent messages | `user` vs `group` prop mixed up | Pass exactly one of `user` OR `group` — never both, never neither |
| "Reply in Thread" option does nothing when tapped | Thread panel not wired | Set `hideReplyInThreadOption` (see `cometchat-native-components` § 11) OR wire a thread panel (§ 2b) |
| `onItemPress` callback not firing | Wrong prop name (RN uses `Press` not `Click`) | It's `onItemPress`, not `onItemClick` — web and RN diverge here |
| Conversations list empty but data exists | Wrong request builder | Check `conversationsRequestBuilder` filters (tags, types) |
| List components collapse to zero height | Container has no bounded height | Wrap in `<View style={{ flex: 1 }}>` or explicit `height: N`. See `cometchat-native-placement` Hard rule #8 |
| Component renders but data never loads | User/group passed as UID string instead of `CometChat.User`/`Group` instance | `await CometChat.getUser(uid)` first, pass the resolved object |
### 2h. Calling
| Symptom | Likely cause | Fix |
|---|---|---|
| Call buttons missing from MessageHeader | `@cometchat/calls-sdk-react-native` not installed | Install + rebuild. See `cometchat-native-features` § 3 |
| Incoming call UI doesn't show | `CometChatIncomingCall` not mounted or listener not registered | Register `CometChat.addCallListener(...)` and mount `<CometChatIncomingCall>` at app root |
| Call connects but no audio/video | Missing Podfile settings or Android SDK versions | Check `IPHONEOS_DEPLOYMENT_TARGET = 12.0` + Android `minSdkVersion = 24` |
| WebRTC errors on load | Missing peer deps | Install `react-native-webrtc` + `@react-native-community/netinfo` + `react-native-background-timer` + `react-native-callstats` |
| Call crashes on Android after accept | Permissions denied at runtime | Request `RECORD_AUDIO` + `CAMERA` runtime permissions before call |
| `onIncomingCallReceived` silent | Listener registered before login completes | Register the listener INSIDE the `useEffect` that runs after login |
### 2i. Extensions
| Symptom | Likely cause | Fix |
|---|---|---|
| Polls option missing from composer | Extension not enabled in dashboard | `cometchat features enable polls --json` or toggle in dashboard → Features |
| Stickers not rendering | Sticker extension not enabled | Same as above — enable via CLI |
| Extension enabled but UI doesn't appear | Cached session — hard reload needed | Stop Metro, clear cache (`--reset-cache`), rebuild |
| Extension says enabled but `auto_wired_in_uikit: false` | Needs `UIKitSettingsBuilder.setExtensions([...])` before init | Add `.setExtensions([new StickersExtension(), ...])` — see `cometchat-native-features` § 2 |
| Message translation "Translate" option missing | Extension not enabled | Enable `MessageTranslation` via CLI / dashboard |
### 2j. AI features
| Symptom | Likely cause | Fix |
|---|---|---|
| AI suggestions don't appear | AI feature not enabled in dashboard | Enable via dashboard → AI → individual feature |
| Smart Replies not showing | Smart Replies extension off | Enable via CLI: `cometchat features enable smart-replies` |
| Conversation Starter missing | Feature off + no conversation context | Enable feature + ensure chat has at least one previous message |
### 2k. Localization
| Symptom | Likely cause | Fix |
|---|---|---|
| UI text not translated | Language code mismatch | Use full codes like `en-US`, not short `en` |
| Auto-detection not working | `react-native-localize` missing | Install: `npm install react-native-localize` + pod install |
| Custom translations not applied | Missing `CometChatI18nProvider` | Wrap app inside `CometChatThemeProvider` with `<CometChatI18nProvider>` |
### 2l. Events
| Symptom | Likely cause | Fix |
|---|---|---|
| Listener doesn't fire | Wrong event name | Check `docs/events.mdx` for exact event names (e.g., `ccMessageSent` not `onMessageSent`) |
| Listener fires twice | Duplicate registration (common with hot reload / remount) | Remove in `useEffect` cleanup: `return () => CometChatUIEventHandler.removeMessageListener(id)` |
| Listener ID collision | Hardcoded ID reused across components | Use a unique constant per component: `const LISTENER_ID = "APP_SCREEN_LISTENER"` |
| Call events not received | `CometChat.addCallListener` registered before login | Move registration into `useEffect` after login completes |
### 2m. Production / auth tokens
| Symptom | Likely cause | Fix |
|---|---|---|
| `login({ authToken })` fails: "user does not exist" | User not created in CometChat before token mint | Create user server-side via REST API on your signup flow. See `cometchat-native-production` § 6 |
| Token endpoint returns 401 | Backend auth check failing | Verify `Authorization: Bearer <jwt>` header is attached to the fetch |
| 429 rate limit on token endpoint | Minting tokens too often (e.g. per screen mount) | Cache client-side, reuse until expiry. See `cometchat-native-production` § 10 |
| SDK disconnects after a few hours | Token expired | Wire `onDisconnected` listener to re-fetch + re-login. See `cometchat-native-production` § 5c |
| `CometChatUIKit.loginWithAuthToken` not found | Wrong API name | It's `CometChatUIKit.login({ authToken })` — same method as dev, different key |
---
## 3. Deep dives on common failures
### 3a. "The most common 'why is my chat blank' bug"
CometChat components fill 100% of their parent's height. If the parent has no bounded height, the components render at 0px and look empty.
Diagnostic:
```bash
grep -B5 -A5 "CometChatMessageList\|CometChatConversations" src/**/*.tsx | grep -B2 -A2 "style"
```
Look for the message list's parent. One of these must be true:
- Parent has `flex: 1`
- Parent has an explicit `height: N`
- Parent is a flex column with the list getting `flex: 1`
Broken example:
```tsx
<ScrollView>
<CometChatMessageList user={user} /> {/* ← ScrollView doesn't constrain height */}
</ScrollView>
```
Fixed:
```tsx
<View style={{ flex: 1 }}>
<CometChatMessageList user={user} hideReplyInThreadOption />
</View>
```
### 3b. Apple Privacy Manifest rejection (`ITMS-91053`)
Apple's App Store Connect rejects RN apps that use certain APIs without declaring them in `PrivacyInfo.xcprivacy`. The CometChat kit (through React Native + `react-native-video`) triggers 4 of these APIs.
Full manifest content is in `cometchat-native-bare-patterns` § 2c. Required reason codes:
| API | Code |
|---|---|
| FileTimestamp | `C617.1` |
| UserDefaults | `CA92.1` |
| SystemBootTime | `35F9.1` |
| DiskSpace | `E174.1` |
If the user's rejection email lists OTHER API categories, their app uses additional Apple APIs through other SDKs (analytics, crash reporters, etc.). Add those codes too — the CometChat-specific 4 aren't exhaustive for all apps.
After updating:
1. `cd ios && pod install && cd ..`
2. Rebuild the archive
3. Resubmit
### 3c. Metro cache issues (post-dep-install "not found" errors)
Happens when you `npm install` a native module and Metro's bundler still has the old module graph cached.
```bash
# Full reset
watchman watch-del-all 2>/dev/null # watchman (if installed)
rm -rf $TMPDIR/metro-* # Metro cache on macOS
rm -rf $TMPDIR/haste-map-* # Haste cache on macOS
rm -rf node_modules
npm install
cd ios && pod install && cd .. # iOS only
npx react-native start --reset-cache # bare
# OR for Expo:
npx expo start --clear
```
If the error persists after a clean cache + pod install, the native module isn't autolinked. Check `react-native.config.js` for exclusions.
### 3d. "App crashes on first launch" — entry file order
`index.js` MUST have `import "react-native-gesture-handler"` as line 1. Not line 2. Not after a `// comment`. Not after a `"use strict"`. Literally line 1.
Wrong:
```js
import React from "react"; // ← line 1
import "react-native-gesture-handler"; // ← line 2 — TOO LATE
```
Right:
```js
import "react-native-gesture-handler"; // ← line 1
import React from "react";
```
For Expo Router, the same rule applies to `app/_layout.tsx`.
### 3e. Push notification issues
Push setup is covered end-to-end in `cometchat-native-push`. That skill
has:
- § 3 APNs p8 setup (Apple Developer portal)
- § 4 FCM setup (Firebase project + service account)
- § 5 CometChat dashboard provider upload (dev + prod for iOS)
- § 7 Client registration with `CometChatNotifications.registerPushToken(token, platform, providerId)` (the correct API — not `CometChat.registerTokenForPushNotification`)
- § 9 Foreground display + tap-to-deep-link handlers
- § 12 Troubleshooting table mapping symptoms to fixes (production APNs cert missing, token registered before login, Expo Go can't receive push, etc.)
When diagnosing a push issue, route the user to `cometchat-native-push § 12`
rather than re-triaging here.
---
## 4. v4 → v5 upgrade gotchas
If the user is upgrading from `@cometchat/chat-uikit-react-native@4`, these are the common breakages. Full migration guide: `docs/upgrading-from-v4.mdx`.
| v4 | v5 | Notes |
|---|---|---|
| `CometChatContext` provider | `CometChatThemeProvider` + `CometChatI18nProvider` | Split into theme + i18n providers |
| `<CometChatConversationsWithMessages>` composite | `<CometChatConversations>` + navigate to separate screen | Composite component removed; use two-screen navigation pattern |
| `theme` prop on components | Theme via `CometChatThemeProvider` only | Per-component theme prop removed |
| `Palette` object | Color tokens under `theme.color` | Renamed + restructured |
| `onClick` callback names | `onPress` callback names | React Native convention |
| `CometChat.login(uid, authKey)` | `CometChatUIKit.login({ uid })` | Object-form argument |
| Native deps in `peerDependencies` | Must explicitly install peer deps | See `cometchat-native-core` § 9 |
| Single NPM package for calls | Separate `@cometchat/calls-sdk-react-native` package | Calls broken out |
### Upgrade sequence
```bash
# 1. Update the main kit
npm install @cometchat/chat-uikit-react-native@latest
# 2. Install the full peer-dep set (v5 doesn't auto-install them like v4 did)
npm install @cometchat/chat-sdk-react-native \
@react-native-async-storage/async-storage \
@react-native-clipboard/clipboard \
@react-native-community/datetimepicker \
react-native-gesture-handler react-native-localize \
react-native-safe-area-context react-native-svg \
react-native-video dayjs punycode
# 3. iOS
cd ios && pod install && cd ..
# 4. Android — add the local Maven repo for async-storage
# (see cometchat-native-bare-patterns § 3b)
# 5. Entry file — add `import "react-native-gesture-handler"` as line 1
# (new requirement in v5)
# 6. Replace `<CometChatContext>` with the 4-wrapper chain
# (see cometchat-native-core § 3)
# 7. Split composite components — e.g. `<CometChatConversationsWithMessages>`
# becomes two screens with navigation
# 8. Rename `onClick*` → `onPress*` throughout
# 9. Replace `Palette` → theme tokens
# 10. Rebuild + test
```
---
## 5. Escalation — when the above doesn't solve it
If none of the lookup tables or deep dives apply:
1. **Read the raw error.** RN errors are usually specific ("Module 'X' not found in app 'Y'" is different from "TurboModuleRegistry.getEnforcing").
2. **Check the dev console + native logs.** For iOS: Xcode → View → Debug Area → Activate Console. For Android: `adb logcat | grep -E "cometchat|CometChat|ReactNative"`.
3. **Search the upstream docs MCP** (`cometchat-docs` if installed).
4. **Search the sample app** (`examples/SampleApp/` or `examples/SampleAppExpo/`) for a working version of the pattern the user is trying.
5. **If the issue is a kit bug**, file at https://github.com/cometchat/cometchat-uikit-react-native/issues with a minimal repro.
---
## 6. Hard rules (diagnostic best-practice)
1. **Don't assume — triage first.** § 1 gets the framework, wrapper chain, pod state, and dep list before proposing a fix. A "I don't see messages" could be 10 different things.
2. **Don't suggest "try reinstalling node_modules" as a first step.** It's rarely the actual fix and it takes minutes; check the entry file, wrapper chain, and pod state first.
3. **Always verify against the ground-truth doc** (`troubleshooting.mdx`). If a user's symptom matches a table row, use the doc's fix verbatim — don't paraphrase.
4. **Never guess a fix that requires code changes without first gathering facts.** Changing code based on a wrong hypothesis wastes user time.
5. **When recommending a rebuild, say why + what to rebuild.** "pod install + rebuild iOS" is more useful than "try rebuilding."
6. **If a symptom matches the Apple Privacy Manifest rejection, always paste the full 4-code XML.** Don't describe it; show it.
---
## Skill routing reference
| Skill | When to route |
|---|---|
| `cometchat-native-core` | Most "doesn't work" bugs trace back here — init/login/wrapper chain |
| `cometchat-native-components` | Wrong prop / slot view / request builder |
| `cometchat-native-placement` | Blank chat / bounded-height issues |
| `cometchat-native-expo-patterns` | Expo-specific build errors (dev client required, `expo install`) |
| `cometchat-native-bare-patterns` | Bare RN build errors (pod install, Android Maven, privacy manifest) |
| `cometchat-native-theming` | Theme not applying, dark mode not switching, font not loading |
| `cometchat-native-features` | Calls don't work, extension UI missing after enable |
| `cometchat-native-customization` | Formatter not rendering, listener not firing, template not showing |
| `cometchat-native-production` | 401 on token fetch, user-does-not-exist on login |
| `cometchat-native-troubleshooting` | This skill — cross-category diagnosis + v4→v5 upgrade |
---
name: cometchat
description: Entry-point for CometChat integration. Guides a multi-step interactive conversation to understand the project, gather requirements, and write production-quality integration code.
license: "MIT"
allowed-tools: "executeBash, readFile, fileSearch, listDirectory, AskUserQuestion"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "cometchat dispatcher entry react chat"
---
## Use this skill when
The user wants to add CometChat to any kind of project. Trigger phrases:
- `/cometchat`
- "add cometchat", "integrate cometchat", "add chat to my app"
- "add messaging", "add chat ui"
This is the **entry point**. Do not invoke framework-specific skills
directly — this dispatcher will route to the right ones.
## How v3 works
v3 skills are **interactive and conversational**. You don't just detect
the framework and dump code. You have a conversation with the developer
to understand their project, their use case, and exactly where chat
should go — THEN you write code that fits.
The pattern skills teach you:
- `cometchat-core` — initialization, login, CSS, env vars, provider pattern
- `cometchat-components` — every component name, props, composition patterns
- `cometchat-placement` — WHERE to put chat (route, modal, drawer, embed, widget)
- `cometchat-{framework}-patterns` — framework-specific integration patterns
**Key principle: ask, don't assume.** Every piece of information you need
from the user should be asked explicitly. Don't guess the route path,
don't guess where the trigger button goes, don't guess the auth system.
## Steps
### Step 1 — Detect framework + map the project
First, check if `.cometchat/config.json` exists:
```bash
npx @cometchat/skills-cli config show --json
```
If config exists with previous answers, tell the user:
> "I see you've set up CometChat before. Using your saved config:
> Framework: {framework}, App: {appId}, Intent: {intent}.
> Want to continue with these, or start fresh?"
If no config, run detection:
```bash
npx @cometchat/skills-cli detect --json
```
**Then read the project yourself — this is critical:**
- `package.json` — name, dependencies, scripts
- The source directory structure — list all directories under `src/` or `app/`
- Find the router: look for `createBrowserRouter`, `app/` directory, `pages/`,
`react-router.config.ts`, `astro.config.*`
- Find the layout: `App.tsx`, `layout.tsx`, `root.tsx`, `Layout.astro`
- Find the nav: look for components with "nav", "header", "sidebar" in name
- Find existing pages/routes: list them so you can reference them later
Store this mental map — you'll use it throughout the conversation.
If `compatibility.supported` is `false`, stop and surface the warnings.
### Step 2 — Set up credentials (onboarding)
**CRITICAL: All onboarding happens via CLI commands. NEVER send the user
to a browser or dashboard. The CLI handles signup, login, app creation,
and credential writing — all from the terminal.**
If config has `appId` set, verify credentials are in `.env` and skip to Step 3.
Otherwise check:
```bash
npx @cometchat/skills-cli auth status --json
```
If `status` is `"logged-in"`, skip to **Step 2c** (app selection).
If `status` is `"logged-out"`, ask:
Use `AskUserQuestion`:
- **question:** "Let's set up CometChat. Do you have an account?"
- **header:** "Account"
- **multiSelect:** false
- **options:**
1. label: "Create a new account", description: "Free signup — I'll handle it right here, no browser needed."
2. label: "Sign in to existing account", description: "Log in and pick one of your apps."
3. label: "I'll paste credentials myself", description: "I already have my App ID, Region, and Auth Key."
Option 1 → **Step 2b**. Option 2 → **Step 2a**. Option 3 → **Step 2d**.
#### Step 2a — Sign in (existing account, browser flow)
```bash
npx @cometchat/skills-cli auth login
```
This command:
1. Generates a short-lived session via the CLI auth API.
2. Opens `https://app.cometchat.com/login?sessionId=<hex>` in the user's default browser.
3. Polls the auth API every 5 seconds for up to 15 minutes.
4. When the user finishes signing in (email+password, Google, or GitHub — whatever their account uses), the dashboard marks the session authenticated. The next poll receives the bearer token and stores it in the OS keychain.
5. Prints `✓ Logged in as <email> (backend: keychain-macos).`
Let the CLI block — do NOT background it, do NOT race it with other
prompts. The user completes sign-in in the browser tab; the terminal
waits.
Terminal error handling (surface verbatim, stop, do not retry silently):
- `ACCESS_DENIED` — user clicked Deny in the dashboard.
- `EXPIRED` — 15-minute window elapsed.
- `TIMEOUT` — max polls exhausted before user authorized.
- `ABORTED` — user Ctrl-C'd the CLI.
- `NETWORK` — couldn't reach the auth host.
- `ALREADY_AUTHENTICATED` — this session was already consumed. Re-run
`auth login` to mint a fresh session.
After success, verify:
```bash
npx @cometchat/skills-cli auth status --json
```
If `status` is `"logged-in"`, proceed to **Step 2c**.
#### Step 2b — Sign up (new account, browser flow)
```bash
npx @cometchat/skills-cli auth signup
```
Same polling flow as Step 2a, but the CLI opens
`https://app.cometchat.com/signup?sessionId=<hex>`. The browser
handles everything — email, name, password, verification email, role,
industry. The CLI never sees any of those values. When the user
finishes signup in the browser, the next poll stores the bearer token
in the OS keychain and the CLI prints `✓ Logged in as <email>`.
No role / name / verification-code questions in the chat. The dashboard
owns that flow now; skipping it keeps the user's password and verification
code out of the transcript.
Error codes match Step 2a (ACCESS_DENIED, EXPIRED, TIMEOUT, ABORTED,
NETWORK, ALREADY_AUTHENTICATED). Surface verbatim and stop.
After success, verify:
```bash
npx @cometchat/skills-cli auth status --json
```
If `status` is `"logged-in"`, proceed to **Step 2c**.
#### Step 2c — Pick or create an app
**Run this immediately — do NOT ask the user to go to any dashboard:**
```bash
npx @cometchat/skills-cli provision list --json
```
**If the user has existing apps**, show them and ask which to use:
> "I found these CometChat apps on your account:
> 1. my-marketplace-chat (us) — Developer plan
> 2. test-app (eu) — Developer plan
>
> Which one should I use, or should I create a new one?"
**For an existing app**, fetch credentials and wire everything in one call
(pass `--framework` from Step 1 detection — one of `reactjs`, `nextjs`,
`react-router`, `astro`):
```bash
npx @cometchat/skills-cli provision setup \
--app-id "<selected-appId>" --framework "<framework>" --json
```
This creates/updates `.env` with the correct prefix AND writes
`.cometchat/config.json` in one step. Output is compact:
`{ appId, region, framework, envFile, configPath }` — no authKey echoed
back, no multi-command chain. Skip ahead to "Tell the user" below.
**If no apps exist** (or user wants new), collect:
1. App name — suggest `<project-name>-chat` from package.json `name`
2. Region — use `AskUserQuestion`:
- **question:** "Which region for your CometChat app?"
- **header:** "Region"
- **options:**
1. label: "US", description: "United States (recommended)"
2. label: "EU", description: "Europe"
3. label: "India", description: "India"
**Region key mapping** (CLI expects lowercase):
| Label | `--region` value |
|---|---|
| US | `us` |
| EU | `eu` |
| India | `in` |
3. Industry — use `AskUserQuestion`:
- **question:** "What's your app's industry?"
- **header:** "Industry"
- **options:**
1. label: "SaaS / Business", description: ""
2. label: "Marketplace", description: ""
3. label: "Social / Community", description: ""
4. label: "Other", description: ""
**Industry key mapping:**
| Label | --industry value |
|---|---|
| SaaS / Business | `saas_businesses` |
| Marketplace | `online_marketplaces` |
| Social / Community | `community_and_social` |
| Healthcare | `healthcare` |
| Dating | `dating` |
| Education | `online_education` |
| Events / Streaming | `events_and_streaming` |
| Sports / Gaming | `sports_and_gaming` |
| Team Communication | `team_comms_and_workflows` |
| On-demand Services | `on_demand_services` |
| Other | `other` |
**Confirm before creating:**
> "I'll create a CometChat app:
> - Name: test-cometchat-vite-chat
> - Region: US
> - Industry: SaaS / Business
>
> Go ahead?"
Then create the app AND wire `.env` AND save config in one step. Pass
`--framework` from Step 1 detection (one of `reactjs`, `nextjs`,
`react-router`, `astro`):
```bash
npx @cometchat/skills-cli provision setup \
--name "<name>" --region "<region>" --industry "<industry_key>" \
--framework "<framework>" --json
```
Output is compact: `{ appId, region, framework, envFile, configPath }`.
The authKey is written to the env file but is NOT echoed to stdout, so
credentials don't appear multiple times in the transcript. This replaces
the old `provision create` → `provision use` → `config init` chain.
Tell the user: "Your CometChat account and app are ready. Credentials
saved to `.env`. Let's set up the integration."
#### Step 2d — Paste keys manually
Tell the user which env vars to set based on the detected framework:
| Framework | Env file | Variables |
|---|---|---|
| reactjs (Vite) | `.env` | `VITE_COMETCHAT_APP_ID`, `VITE_COMETCHAT_REGION`, `VITE_COMETCHAT_AUTH_KEY` |
| nextjs | `.env.local` | `NEXT_PUBLIC_COMETCHAT_APP_ID`, `NEXT_PUBLIC_COMETCHAT_REGION`, `NEXT_PUBLIC_COMETCHAT_AUTH_KEY` |
| react-router | `.env` | `VITE_COMETCHAT_APP_ID`, `VITE_COMETCHAT_REGION`, `VITE_COMETCHAT_AUTH_KEY` |
| astro | `.env` | `PUBLIC_COMETCHAT_APP_ID`, `PUBLIC_COMETCHAT_REGION`, `PUBLIC_COMETCHAT_AUTH_KEY` |
> "Grab your credentials from https://app.cometchat.com → Your App →
> API & Auth Keys. Create the env file above and tell me when done."
After they confirm, verify:
```bash
npx @cometchat/skills-cli config init --json
```
### Step 3 — Interactive requirements gathering
This is the core of v3. A multi-step conversation that gathers everything
you need before writing a single line of code.
#### 3a. "What are you building?"
If config has `intent` set, confirm it and move on.
Otherwise, use `AskUserQuestion`:
- **question:** "What kind of app are you building?"
- **header:** "Your app"
- **multiSelect:** false
- **options:**
1. label: "Messaging app", description: "Chat is the main feature — like Slack, Discord, or WhatsApp."
2. label: "Marketplace or platform", description: "Buyers and sellers communicate — like Airbnb, eBay, or Fiverr."
3. label: "SaaS or dashboard", description: "Team chat or support chat inside a product — like Notion or Intercom."
4. label: "Social or community", description: "User profiles with messaging — like a dating app or forum."
5. label: "Support or helpdesk", description: "Customer-to-agent communication."
6. label: "Just exploring", description: "Quick demo — fastest path to see chat working."
**If "Just exploring":** skip the rest of Step 3. Use `cometchat apply`
demo mode in Step 5.
#### 3b. Show what you recommend and why
Based on the intent, present the recommendation:
| Intent | What you'll set up |
|---|---|
| **Messaging app** | A dedicated messages page at a route you choose. Two-pane: conversation list + active chat. |
| **Marketplace** | A "Chat with seller" drawer on your product page + an inbox page at /messages. |
| **SaaS / dashboard** | A chat modal triggered from your navbar + a full messages page. |
| **Social / community** | A full messenger page with tabs: Chats, Calls, Users, Groups. |
| **Support** | A floating widget bubble in the bottom-right corner. |
When explaining, reference the ASCII art from `cometchat-placement`
("Visual reference — experience layouts") so the user can visualize it.
Ask: "Does this sound right, or do you want a different approach?"
Let them override.
#### 3c. Ask where things should go
**Show the user their actual project structure** — list the pages/routes
you found in Step 1. Then ask placement-specific questions:
**For Route placement (messaging, social):**
> "I found these pages in your project:
> - / (home)
> - /about
> - /products
> - /profile
>
> Where should the messages page live?"
Default suggestion: `/messages`. Let user type a custom path.
**For Drawer placement (marketplace):**
> "Which page should have the 'Chat' button that opens the drawer?
> I found these pages:
> - app/products/[id]/page.tsx
> - app/listings/page.tsx
> - app/profile/[id]/page.tsx
>
> Which one?"
After they pick, read that page file. Look for existing buttons,
actions, or interactive elements. Ask:
> "I see a 'Contact Seller' button in ProductDetail.tsx at line 45.
> Should I wire the chat drawer to that button, or add a new one?"
**For Modal placement (SaaS):**
> "Where should the 'Open chat' button go? I found these components
> that look like navigation:
> - src/components/Navbar.tsx
> - src/components/Sidebar.tsx
>
> Which one should have the chat trigger?"
**For Widget placement (support):**
> "Should the widget appear on all pages, or only specific ones?"
**For combinations (marketplace = drawer + route):**
Ask both questions in sequence. The drawer and route are separate
components wired into separate places.
#### 3d. Detect and ask about authentication
Read the project's `package.json` and source files. Look for auth:
- `next-auth` / `@auth/core` → NextAuth
- `@clerk/nextjs` / `@clerk/clerk-react` → Clerk
- `@supabase/supabase-js` + auth usage → Supabase Auth
- `firebase` / `firebase/auth` → Firebase Auth
- `passport` → Passport.js
- `jsonwebtoken` / `jose` → Custom JWT
- None detected → no auth
Report what you found and ask:
If auth detected:
> "I see you're using [NextAuth / Clerk / etc.]. Here's how CometChat
> will work with it:
>
> - **Development (now):** I'll use CometChat's Auth Key for quick
> testing with pre-seeded users (cometchat-uid-1, uid-2, etc.)
> - **Production (later):** Your server will call CometChat's REST API
> to generate per-user auth tokens. I can set this up now or later.
>
> Start with dev mode for now? You can upgrade to production auth
> anytime by choosing 'Set up production auth' from the menu."
If no auth detected:
> "I don't see an authentication system in your project yet. For now,
> I'll set up CometChat with a hardcoded test user (cometchat-uid-1).
>
> When you add auth later, run `/cometchat` again and choose
> 'Set up production auth' to connect them."
#### 3e. Ask about user mapping (if auth detected)
If the user has auth AND wants to set up production mode now:
> "How should your app's users map to CometChat users?
>
> 1. Use your existing user ID as the CometChat UID (simplest)
> 2. Generate a separate CometChat UID and store it in your database
> 3. Let me just set up dev mode for now
>
> Option 1 works if your user IDs are alphanumeric strings (no spaces,
> no special characters). What does a typical user ID look like in
> your system?"
If they share an example, validate it's CometChat-compatible
(alphanumeric, underscores, hyphens — no spaces or special chars).
#### 3f. Confirm the plan
**This is critical. Show EXACTLY what you'll do before doing it.**
> "Here's what I'll create:
>
> **New files:**
> - `app/providers/CometChatProvider.tsx` — initialization + login
> - `app/messages/page.tsx` — inbox with conversation list + message view
> - `app/components/ChatDrawer.tsx` — slide-out drawer for product page chat
> - `.env.local` — CometChat credentials (already filled)
>
> **Files I'll modify:**
> - `app/products/[id]/page.tsx` — add ChatDrawer import + trigger button
> - `app/layout.tsx` — wrap children with CometChatProvider
> - `app/components/Navbar.tsx` — add 'Messages' link
>
> **Files I will NOT touch:**
> - `app/page.tsx` (your home page)
> - Any other existing pages
>
> **Dependencies to install:**
> - @cometchat/chat-sdk-javascript
> - @cometchat/chat-uikit-react
>
> **Auth mode:** Development (Auth Key). Upgrade to production
> with `/cometchat` → 'Set up production auth' when ready.
>
> Proceed? [y/n]"
Wait for explicit confirmation. If the user says no or wants changes,
go back to the relevant question and re-ask.
### Step 4 — Reference pattern skills
**All 13 skills are already loaded in your context** as `.claude/skills/`
files. Do NOT use the `Skill()` tool — that's for a different system.
Instead, simply read and follow the instructions in these skills:
1. `cometchat-core` — initialization, provider, CSS, anti-patterns
2. `cometchat-components` — component catalog, composition patterns
3. Framework skill for the detected framework:
- `reactjs` → `cometchat-react-patterns`
- `nextjs` → `cometchat-nextjs-patterns`
- `react-router` → `cometchat-react-router-patterns`
- `astro` → `cometchat-astro-patterns`
4. `cometchat-placement` — placement pattern for the chosen approach
These are reference documents in your context, not tool calls.
### Step 5 — Write the integration
Execute the confirmed plan. For each file:
1. **CometChatProvider** — follow the framework skill's provider pattern.
Use the correct env var prefix. Module-level `initialized` guard.
Mount at the level agreed in Step 3f.
2. **Chat component(s)** — follow the placement skill's pattern.
Use the component compositions from the components skill.
If drawer/modal: connect to the specific user/group the user specified.
3. **Wire into existing project** — READ each file before modifying:
- Router: add the route entry. Show the user the diff.
- Nav: add the link. Show the user the diff.
- Trigger page: add the drawer/modal import + trigger button. Show diff.
4. **CSS import** — add once at the root level per framework conventions.
5. **Environment variables** — write `.env` with the correct prefix.
If auth key is already there from the wizard, don't duplicate.
6. **Install dependencies:**
```bash
npm install @cometchat/chat-sdk-javascript @cometchat/chat-uikit-react
```
7. **Update config.json** — save all the choices in one call:
```bash
npx @cometchat/skills-cli config save \
--intent "<intent>" \
--experience <n> \
--placement "<type>" \
--placement-path "<path>" \
--auth-mode "<mode>" --json
```
Pass only the fields you have — `config save` accepts any subset.
This replaces the old 5-command `config set k v` chain. Omit
`--experience` in the AI-written path (it only applies to CLI-
generated experiences 1/2/3).
8. **Record state so Phase B commands work — DO NOT SKIP.** Every
Phase B command (`info`, `status`, `doctor`, `verify`, `uninstall`,
`apply-theme`, `apply-feature`, `add-widget`, `add-user-mgmt`,
`production-auth`) reads `.cometchat/state.json` to know what the
integration looks like. Without this step, every one of them reports
"not integrated in this project" even though the code is there, and
the user can't iterate on their integration at all.
Pass every file you wrote (owned) and every existing file you patched:
```bash
npx @cometchat/skills-cli state record \
--framework "<framework>" \
--placement "<type>" \
--placement-path "<path>" \
--auth-mode "<mode>" \
--files-owned "src/providers/CometChatProvider.tsx,src/components/ChatDrawer.tsx,src/pages/MessagesPage.tsx" \
--files-patched "src/main.tsx:v3/main.tsx,src/App.tsx:v3/App.tsx,src/components/Layout.tsx:v3/Layout.tsx" \
--json
```
- `--files-owned` — comma-separated list of every NEW file you wrote
(the provider, drawer, inbox page, etc.). The CLI computes SHA-256
checksums for each so it can detect drift later.
- `--files-patched` — comma-separated `path:patch_id` pairs for every
EXISTING file you modified (main.tsx, App.tsx, Layout.tsx, nav, the
trigger page). `patch_id` can be any stable label — `v3/<filename>`
is a reasonable default.
If this call errors (CLI flag mismatch, missing --framework, etc.),
surface the error and retry with the correct flags rather than moving
on. A completed Phase A with a missing state.json is worse than a
visible error — the user discovers the breakage later when they try
to add a feature or run diagnostics.
**Exception — "Just exploring" / demo mode:**
```bash
npx @cometchat/skills-cli apply --experience 1 --framework <detected>
npx @cometchat/skills-cli verify --json
npx @cometchat/skills-cli install
```
### Step 6 — Verify + show result
Run a TypeScript check to verify the code compiles:
```bash
npx tsc --noEmit
```
**Do NOT run `npx @cometchat/skills-cli verify`** — it checks for
CLI-generated `.cometchat/state.json` which doesn't exist in v3
(AI writes code directly, not via `cometchat apply`). Use `tsc` instead.
Surface any issues. Then:
> "CometChat is integrated! Here's what was set up:
>
> - Messages page at /messages ✓
> - Chat drawer on product page ✓
> - Provider + CSS wired ✓
> - Dependencies installed ✓
>
> Run `npm run dev` and try it out. Pre-seeded test users
> (cometchat-uid-1 through uid-5) are ready to chat.
>
> What would you like to do next?"
### Step 7 — Iteration menu
Use `AskUserQuestion`:
- **question:** "What would you like to do next?"
- **header:** "Next step"
- **multiSelect:** false
- **options:**
1. label: "Customize look and feel (themes)", description: "Pick a preset (slack, whatsapp, imessage, discord, notion) or set brand colors."
2. label: "Add a feature", description: "Browse ~35 features — calls, reactions, polls, AI, and more."
3. label: "Customize a component", description: "Custom bubbles, headers, composer actions, details views — I'll read the docs and write it."
4. label: "Add a floating chat widget", description: "An overlay button + drawer on top of your existing app."
5. label: "Set up production auth", description: "Replace the dev Auth Key with a server-side token endpoint. Read `cometchat-production` skill."
6. label: "Set up user management", description: "Server endpoints for creating, updating, deleting CometChat users."
7. label: "Run diagnostics", description: "Check for drift, missing env vars, broken imports."
8. label: "I'm done", description: "Exit."
For **component customization**: read `cometchat-components` + docs MCP,
then write the customization code directly. This is pure AI work — no
CLI command. Ask the user what they want to customize, read the relevant
component's props from the catalog, and propose changes.
For **production auth**: read the `cometchat-production` skill (already
in your context). It's interactive — ask the user about their auth
system and generate the server-side token endpoint for their framework.
### Re-rendering the menu after each action
After every Phase B action completes, you **MUST** re-invoke
`AskUserQuestion` with the **exact same 8 options** listed above
(same `question`, `header`, `multiSelect: false`, same option labels
and descriptions — verbatim). This gives the user arrow-key selection
in their terminal.
**Do NOT:**
- Present the options as a prose bullet list (`"What would you like to
do next?\n - Customize the theme...\n - Add calls..."`) — this
forces the user to type their answer, which is a worse UX.
- Invent new options based on what the user just did (e.g. "Customize
theme to match Nestly's brand", "Swap the drawer header for a custom
view"). The 8 options above are the canonical set and don't change
between iterations.
- Skip the menu and ask a freeform "What's next?" — always route
through `AskUserQuestion`.
- Drop options or add new ones. The user expects the same 8 choices
every time, even if some are redundant with what they just did
(they may want to do the same kind of action twice, e.g. add two
features).
The iteration loop is the whole point of Phase B. Re-rendering the
canonical menu via `AskUserQuestion` after every action is how the
user controls the session.
## Hard rules
- **Ask, don't assume.** Every integration decision should be confirmed.
- Always run `detect` first. Do not assume the framework.
- Always use `npx @cometchat/skills-cli` for CLI commands.
- NEVER replace existing project files unless the user chose demo mode.
- ALWAYS read existing files before modifying them.
- ALWAYS show the plan (Step 3f) and get confirmation before writing.
- For component names and props, use the `cometchat-components` skill
or docs MCP — never invent from training data.
- After writing code, update `.cometchat/config.json` with the choices made.
- **NEVER use the `Skill()` tool** to load CometChat skills. All 13
skills are already in your context as `.claude/skills/` files. Just
read and follow them directly.
## Error handling
If the CLI's `--json` output includes `human_message` / `suggestion` fields,
show those to the user. Then show the raw `error` in parentheses for
debuggability. If `retryable: false`, do NOT offer a retry.
## Optional: docs MCP
For deeper component customization:
```
claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp
```
Not required for integration or Phase B CLI flows.
## Skill routing reference
| Skill | When to load |
|---|---|
| `cometchat-core` | Always — before any integration code |
| `cometchat-components` | Always — before writing component code |
| `cometchat-placement` | When integrating — for placement patterns |
| `cometchat-react-patterns` | framework = reactjs |
| `cometchat-nextjs-patterns` | framework = nextjs |
| `cometchat-react-router-patterns` | framework = react-router |
| `cometchat-astro-patterns` | framework = astro |
| `cometchat-theming` | When customizing themes |
| `cometchat-features` | When adding features |
| `cometchat-production` | When setting up production auth |
| `cometchat-troubleshooting` | When diagnosing problems |