@cometchat/skills-native
Advanced tools
+41
-194
@@ -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 | |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1
-50%1
-66.67%10380
-96.55%4
-76.47%48
-73.48%2
Infinity%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed