cometchat-native-features-f7ad43 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cometchat-native-features-f7ad43 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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/[email protected] exports.
Every CometChat feature falls into exactly one of these five mechanism categories — these tell you how to wire each feature (the actionable RN view). They map onto the product's canonical 5-tier "work-needed" model (Core Zero-Setup → Builder-Enabled → Config-Only → Config+Settings → SDK-Integrated; see cometchat-features §3 + packages/registry/v6/features/catalog.json; canonical public matrix → Features & Extensions Guide, which outranks this snapshot on conflict): Default ≈ Core; Extension/AI ≈ Config-Only / Config+Settings; Package-install + Component-swap ≈ SDK-Integrated. 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 |
| Extension | Pure boolean backend toggle. CLI flips it via the dashboard API; UI Kit auto-wires the feature once enabled. | Polls, stickers, message translation, link previews, collaborative whiteboard, collaborative document, thumbnail generation | cometchat apply-feature <id> → hard-reload the app |
| AI feature | Backend AI toggle that requires an OpenAI API key on the app. CLI sets the key + flips the toggle in one call. | Smart replies, conversation summary, conversation starter | cometchat apply-feature smart-replies --openai-key sk-… |
| 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".
apply-feature)Most extensions (polls, stickers, translation, link preview, collaborative doc/whiteboard, thumbnails) are pure boolean toggles. Use the CLI:
cometchat apply-feature polls --json
cometchat apply-feature link-preview --jsonThe CLI reads the app ID from .cometchat/state.json (RN goes through cometchat apply), or pass --app-id explicitly. Bearer is from the OS keychain (cometchat auth login once per machine).
These need an OpenAI API key. The CLI sets the key + flips the toggle in one call:
cometchat apply-feature smart-replies --openai-key sk-...Once any AI feature is enabled the key is stored on the app, so subsequent ai-feature applies don't need --openai-key repeated.
Get an OpenAI key at https://platform.openai.com/api-keys.
--json)"status": "applied" → done. Hard-reload (stop Metro + restart + rebuild if on iOS)."status": "already-applied" → already in the desired state."status": "auth-required" → cometchat auth login first."status": "openai-key-required" → re-run with --openai-key sk-…."status": "manual-action-required" → dashboard-only feature (Giphy, Stipop, Tenor, Chatwoot, Intercom, message-shortcuts, disappearing-messages). Surface the next_steps verbatim — these need third-party config the user has to provide manually."status": "error" → surface next_steps — includes the dashboard URL as a fallback.Only when the CLI returns error or isn't available:
| 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 |
auto_wired_in_uikit: falseA minority of extensions need extra wiring via the extensions field on CometChatUIKit.init(). The CLI flags this in its success response:
{
"status": "enabled",
"name": "stickers",
"auto_wired_in_uikit": false,
"next_steps": [
"Pass the extension via the `extensions` field on CometChatUIKit.init({ ... })"
]
}If auto_wired_in_uikit is false, import the matching ExtensionsDataSource from @cometchat/chat-uikit-react-native and pass it via the flat extensions field:
import {
CometChatUIKit,
StickersExtension,
PollsExtension,
} from "@cometchat/chat-uikit-react-native";
await CometChatUIKit.init({
appId: APP_ID,
region: REGION,
authKey: AUTH_KEY,
subscriptionType: "ALL_USERS",
extensions: [new StickersExtension(), new PollsExtension()], // ← new
});Query the docs MCP for the exact extension class name if you don't remember it — extensions.mdx lists all of them.
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.
Expo (managed workflow):
npm install @cometchat/calls-sdk-react-native@^5
npx expo install \
@react-native-community/netinfo \
react-native-background-timer \
react-native-callstats \
react-native-webrtc
npx expo prebuild --cleanBare RN:
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 ..iOS — ios/<App>/Info.plist:
<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:
<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" />Calls SDK requires iOS 12+ and specific Podfile flags. Add to ios/Podfile:
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
endAndroid (android/app/build.gradle):
android {
compileSdkVersion 33
defaultConfig {
minSdkVersion 24
targetSdkVersion 33
}
}All three call surfaces — <CometChatIncomingCall>, <CometChatOutgoingCall>, <CometChatOngoingCall> — are parent-controlled. None auto-mount the others. The parent must register both CometChat.addCallListener (SDK socket; surfaces incoming calls from the network) and CometChatUIEventHandler.addCallListener (UI event bus; surfaces outgoing calls fired by <CometChatCallButtons> / <CometChatMessageHeader>). Add this once at the app root (typically in App.tsx or Expo Router's _layout.tsx). Validated 2026-05-26 against @cometchat/[email protected] end-to-end on Pixel 3 ([[project_v4_3_f75_rn_call_ui_missing]]).
import React, { useEffect, useState } from "react";
import { StyleSheet, View } from "react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { CometChatCalls } from "@cometchat/calls-sdk-react-native";
import {
CometChatIncomingCall,
CometChatOutgoingCall,
CometChatOngoingCall,
CometChatUIEventHandler,
} from "@cometchat/chat-uikit-react-native";
const CALL_LISTENER_ID = "APP_CALL_LISTENER";
function CallEventsProvider({ children }: { children: React.ReactNode }) {
const [incomingCall, setIncomingCall] = useState<CometChat.Call | null>(null);
const [outgoingCall, setOutgoingCall] = useState<CometChat.Call | null>(null);
const [ongoingCall, setOngoingCall] = useState<CometChat.Call | null>(null);
useEffect(() => {
// SDK socket — incoming side
CometChat.addCallListener(
CALL_LISTENER_ID,
new CometChat.CallListener({
onIncomingCallReceived: (call: CometChat.Call) => setIncomingCall(call),
onIncomingCallCancelled: () => setIncomingCall(null),
onOutgoingCallAccepted: () => {}, // kit owns the transition
onOutgoingCallRejected: () => setOutgoingCall(null),
})
);
// UI event bus — outgoing side (fired by CallButtons / MessageHeader)
CometChatUIEventHandler.addCallListener(CALL_LISTENER_ID, {
ccOutgoingCall: ({ call }) => setOutgoingCall(call),
ccCallEnded: () => {
setOutgoingCall(null);
setIncomingCall(null);
setOngoingCall(null);
},
ccShowOngoingCall: ({ call }) => setOngoingCall(call),
});
return () => {
CometChat.removeCallListener(CALL_LISTENER_ID);
CometChatUIEventHandler.removeCallListener(CALL_LISTENER_ID);
};
}, []);
return (
<>
{children}
{incomingCall && (
<View style={StyleSheet.absoluteFill}>
<CometChatIncomingCall
call={incomingCall}
onDecline={() => setIncomingCall(null)}
onError={() => setIncomingCall(null)}
/>
</View>
)}
{outgoingCall && (
<View style={StyleSheet.absoluteFill}>
<CometChatOutgoingCall call={outgoingCall} />
</View>
)}
{ongoingCall && (
<View style={StyleSheet.absoluteFill}>
{/* CometChatOngoingCallInterface = { sessionID (required), callSettingsBuilder (required), onError? } — there is NO `call` prop (verified vs uikit-react-native-v5 CometChatOngoingCall.tsx) */}
<CometChatOngoingCall
sessionID={ongoingCall.getSessionId()}
callSettingsBuilder={new CometChatCalls.CallSettingsBuilder().setIsAudioOnlyCall(ongoingCall.getType() === "audio")}
onError={() => setOngoingCall(null)}
/>
</View>
)}
</>
);
}DO NOT pass `onAccept` to `<CometChatIncomingCall>` — short-circuits the kit's internalacceptCall+ OngoingCall transition. The kit firesccShowOngoingCallafteracceptCallresolves; the listener above wires that intosetOngoingCall, which mounts<CometChatOngoingCall>. Handle onlyonDecline+onErrorhere. Seecometchat-native-calls§1.8.c.
DO NOT skip `CometChatUIEventHandler.addCallListener`. Without it, tapping video/voice on <CometChatMessageHeader> fires WebRTC + camera at the native layer but no overlay UI ever mounts — the user sees nothing change after the tap. This was [[project_v4_3_f75_rn_call_ui_missing]] (F75).Wrap the app (inside the existing provider chain, below CometChatProvider):
<CometChatProvider ...>
<CallEventsProvider>
<AppNavigator />
</CallEventsProvider>
</CometChatProvider>Once the calls SDK is installed, CometChatMessageHeader auto-renders voice + video call buttons. To customize (e.g., hide one):
<CometChatMessageHeader
user={selectedUser}
hideVoiceCallButton={false}
hideVideoCallButton={false}
AuxiliaryButtonView={({ user, group }) => (
// Slot views receive ONE destructured object ({ user, group }) — NOT positional args.
// CometChatCallButtons has NO onVoiceCallPress/onVideoCallPress — it initiates the call
// itself and emits ccOutgoingCall / ccShowOngoingCall on the UI event bus. Mount the
// in-call surface from those listeners (§3d), not from a press callback.
<CometChatCallButtons user={user} group={group} />
)}
/>Navigate to a dedicated screen that hosts CometChatOngoingCall when a call connects:
// 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}
// REQUIRED: a CometChatCalls.CallSettingsBuilder instance (the kit calls .build() on it).
callSettingsBuilder={new CometChatCalls.CallSettingsBuilder().setIsAudioOnlyCall(session.type === "audio")}
onError={() => navigation.goBack()}
/>
// There is no `callType`/`onCallEnded` prop. End-of-call navigation is driven by the
// ccCallEnded listener (§3d) resetting the parent's state.
);
}A history view of past calls. Typically one tab in a tab-based layout (see cometchat-native-placement § 2):
import { CometChatCallLogs } from "@cometchat/chat-uikit-react-native";
export function CallLogsScreen() {
return <CometChatCallLogs onItemPress={(callLog) => openCallDetails(callLog)} />;
}expo run:ios / run:android; bare: npx react-native run-ios / run-android)CometChatIncomingCall within a few secondsCometChatOngoingCallIf incoming calls don't show: listener not registered, or the listener ID collides. See cometchat-native-troubleshooting.
Real WebRTC calls can't run in Jest or Maestro — they need two real devices plus a TURN server. What you CAN test:
CometChatMessageHeader with a userprop 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).
CometChat.addCallListenerfrom the SDK mock and assert your registerCallListener() fires exactly once per mount.
<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.
During an active call, users can chat without leaving the call UI. This is a toggle on the ongoing-call component:
<CometChatOngoingCall
sessionID={session.sessionId}
// REQUIRED — a CometChatCalls.CallSettingsBuilder (enable in-call chat via its setter).
callSettingsBuilder={callSettingsBuilder}
onError={() => navigation.goBack()}
/>In-call chat adds a collapsible chat panel to the call screen. Participants see messages for the duration of the call.
The AI Agent integration adds an AI-powered conversational assistant to your app. Two parts:
ai-support-agent)Once the agent exists in the dashboard, users can message it like any other user.
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.
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.
See ai-assistant-chat-history.mdx and guide-ai-agent.mdx in the docs for:
setAIAssistantTools())CometChatAIAssistantMessageBubble)These are advanced topics — query the docs MCP for the current API if the user wants any of them.
Smart replies is an ai-feature. Enable with one CLI call (the first time also sets the OpenAI key on the app):
cometchat apply-feature smart-replies --openai-key sk-...After enabling, no code changes are required — CometChatMessageComposer automatically renders suggested replies as chips above the input.
For a custom UI — e.g. inline chips inside a custom bubble, only on certain conversation types, or styled to match your design system — read the extension data straight off the incoming message and render your own chips:
import { TouchableOpacity, View, Text } from "react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
interface SmartReply {
reply_positive?: string;
reply_neutral?: string;
reply_negative?: string;
}
export function SmartReplyChips({
message,
onPick,
}: {
message: CometChat.BaseMessage;
onPick: (text: string) => void;
}) {
const metadata = message.getMetadata() as Record<string, unknown> | undefined;
const extensions = (metadata?.["@injected"] as Record<string, unknown>)?.["extensions"] as
| Record<string, unknown>
| undefined;
const smartReply = extensions?.["smart-reply"] as SmartReply | undefined;
if (!smartReply) return null;
const replies = [
smartReply.reply_positive,
smartReply.reply_neutral,
smartReply.reply_negative,
].filter(Boolean) as string[];
return (
<View style={{ flexDirection: "row", gap: 8, padding: 8 }}>
{replies.map((r) => (
<TouchableOpacity
key={r}
onPress={() => onPick(r)}
style={{ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: "#EAEAEA" }}
>
<Text>{r}</Text>
</TouchableOpacity>
))}
</View>
);
}Smart replies are server-generated — the AI runs on CometChat's backend and attaches results to messages via the @injected.extensions.smart-reply metadata path. Your code just reads it.
Common gotchas:
@injected.extensions.smart-reply — not at the top level. Wrong path = undefined.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?":
CometChatMentionsFormatter in textFormatters, already in default config)CometChatSearch component — scoped or global)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.
Presence indicators appear automatically on avatars in CometChatConversations, CometChatUsers, and CometChatGroupMembers — no setup. For custom UI that needs a specific user's status — e.g. a "Sold by Aria Chen · online now" label on a product screen — subscribe with the SDK directly:
import { useEffect, useState } from "react";
import { CometChat } from "@cometchat/chat-sdk-react-native";
export function useUserPresence(uid: string): "online" | "offline" | "unknown" {
const [status, setStatus] = useState<"online" | "offline" | "unknown">("unknown");
useEffect(() => {
let cancelled = false;
// 1. Initial fetch
CometChat.getUser(uid).then((u) => {
if (cancelled) return;
setStatus(u.getStatus() === "online" ? "online" : "offline");
});
// 2. Live updates
const listenerId = `presence-${uid}`;
CometChat.addUserListener(
listenerId,
new CometChat.UserListener({
onUserOnline: (user: CometChat.User) => {
if (user.getUid() === uid) setStatus("online");
},
onUserOffline: (user: CometChat.User) => {
if (user.getUid() === uid) setStatus("offline");
},
}),
);
return () => {
cancelled = true;
CometChat.removeUserListener(listenerId);
};
}, [uid]);
return status;
}Use it: const status = useUserPresence(product.sellerUid);
Common gotchas:
CometChat.getUser(uid) instead.getStatus() returns "online" or "offline" only. For a "last seen 5 min ago" label use getLastActiveAt() (UNIX timestamp).removeUserListener on unmount, or you'll leak listeners across screen re-renders.When a user asks for a feature, use this flow:
hide* prop is turning it off.cometchat apply-feature <id>.3a. Is it smart replies, conversation summary, or conversation starter? → AI feature (§ 2). Run cometchat apply-feature <id> --openai-key sk-.... 3b. Is it Giphy / Stipop / Tenor / Chatwoot / Intercom / Disappearing Messages / Message Shortcuts? → Dashboard-only (§ 2). User must enter third-party config in https://app.cometchat.com → Chat & Messaging → Features.
cometchat-native-customization or cometchat-native-theming.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.| 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 |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.