cometchat-native-calls
CometChat Calls SDK integration for React Native (Expo managed + bare CLI). Covers @cometchat/calls-sdk-react-native install, dual-SDK init, native module linking (iOS pods, Android Gradle), VoIP push via react-native-callkeep + react-native-voip-push-notification + @react-native
What it does
Purpose
Production-grade voice + video calling for React Native (Expo managed + bare CLI). Loaded by cometchat-calls when framework is expo or react-native. Operates in two modes:
- Standalone — calls is the product. Chat SDK (signaling) + Calls SDK (WebRTC) + your own RN screens. VoIP push is mandatory — same rule as native iOS / Android.
- Additive — calls layered onto an existing CometChat React Native UI Kit integration. Adds call buttons inline, mounts
CometChatIncomingCallat app root.
Read these other skills first:
cometchat-calls— dispatcher (modes, hard rules, anti-patterns)cometchat-native-core— Chat SDK init, login, env conventions, gesture handler peer-dep rules- Framework path:
cometchat-native-expo-patterns(managed) ORcometchat-native-bare-patterns(CLI)
Ground truth:
- SDK source —
~/Downloads/calls-sdk/calls-sdk-react-native-5/package/ - Sample app —
~/Downloads/calls-sdk/calls-sdk-react-native-5/sample-apps/cometchat-calls-sample-app-react-native/ - Public docs — https://www.cometchat.com/docs/calls/react-native/overview
1. Hard rules — RN specialization
1.0 Calls SDK login is its own step (v5+)
The v5 Calls SDK has its own auth state, separate from the Chat SDK. After CometChat.login(uid, AUTH_KEY) succeeds, you MUST also call await CometChatCalls.login(uid, AUTH_KEY) — without it, the FIRST calls API call (initiateCall, joinSession, generateToken) throws "auth token cannot be null". The chat skill's "login once, persist forever" pattern does NOT transfer.
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { CometChatCalls } from "@cometchat/calls-sdk-react-native";
// ✓ RIGHT — chat login first, then calls login
await CometChat.login(uid, AUTH_KEY);
try {
const callUser = await CometChatCalls.login(uid, AUTH_KEY); // dev mode
// OR for production:
// const callUser = await CometChatCalls.loginWithAuthToken(authToken);
} catch (e) {
// surface to user — common cause: typo in app id / auth key
}
Surprises that bite on real devices:
- The Chat SDK persists login across launches via AsyncStorage; the Calls SDK does NOT. Even if
CometChat.getLoggedinUser()returns a non-null user on cold start, callCometChatCalls.loginagain before any calls API works. - A single-arg
CometChatCalls.login(uid)overload exists for re-login when the SDK has cached auth — default to the (uid, AUTH_KEY) form for dev to avoid foot-guns. - Login errors surface as Promise rejections — wrap in try/catch.
1.1 Dual-SDK contract
Same shape as web. @cometchat/chat-sdk-react-native initiates ringing; @cometchat/calls-sdk-react-native runs the WebRTC session. Both packages.
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { CometChatCalls } from "@cometchat/calls-sdk-react-native";
// Chat SDK — initiate
const outgoing = new CometChat.Call(receiverUid, CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER);
const initiated = await CometChat.initiateCall(outgoing);
// Calls SDK — generate the token (v5 takes only sessionId; auth is internal after login).
const { token: callToken } = await CometChatCalls.generateToken(initiated.getSessionId());
// On RN, session/join is rendered via the SDK's declarative Component.
// There is NO imperative joinSession(token, settings, viewRef) on RN — the
// Component IS the call surface. With the kit, <CometChatOngoingCall />
// wraps this internally. For custom UI, render <CometChatCalls.Component
// callToken={callToken} /> directly. See references/custom-ui.md.
1.2 VoIP push — react-native-callkeep + platform-specific push
VoIP push on RN is the highest-effort piece. The standard production stack:
react-native-callkeep— bridges CallKit (iOS) + ConnectionService (Android). Single API for "report incoming call to OS"react-native-voip-push-notification— iOS PushKit token registration + payload delivery@react-native-firebase/messaging— Android FCM high-priority data messages- Server side — your push server must split iOS sends to PushKit (VoIP cert) and Android sends to FCM with
priority: "high"and adatapayload (NOTnotification, which ConnectionService can't intercept)
The skill scaffolds all four pieces in standalone mode. Additive mode prompts before adding (it's substantial).
1.3 Foreground service — same Android 14+ rules as native
When the call is active on Android, an ongoing-call foreground service must run. react-native-callkeep handles registration but the app's AndroidManifest.xml must declare:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
Same silent-crash failure mode as native Android (cf. cometchat-android-v5-calls rule 1.3).
For Expo managed, these go into app.json expo.android.permissions AND require a config plugin (react-native-callkeep's plugin) to merge into the generated AndroidManifest.xml during prebuild. Bare RN can edit the manifest directly.
1.4 Server-minted auth tokens
Same — cometchat-native-production covers it.
1.5 Hangup cleanup — RTCPeerConnection + audio session
Combined web + iOS rules. RN's WebRTC bridge wraps both:
function endCall() {
CometChatCalls.leaveSession(); // v5 canonical (endSession is a deprecated shim)
RNCallKeep.endCall(callUUID); // tells CallKit/ConnectionService the call ended
// iOS: matching audio session deactivation happens inside callkeep
// Android: foreground service stop happens inside callkeep
}
Skipping RNCallKeep.endCall leaves the system call UI stuck (lock-screen card persists, OS thinks there's an active call). Common bug.
1.6 Permissions
Required:
- iOS
Info.plist—NSCameraUsageDescription,NSMicrophoneUsageDescription(same as native) - Android — runtime requests via
PermissionsAndroid.requestMultipleforRECORD_AUDIO,CAMERA,POST_NOTIFICATIONS(Android 13+) - Expo managed — declare in
app.jsonexpo.ios.infoPlistandexpo.android.permissions; the prebuild merges into native manifests
1.7 IncomingCall mounted at app root
<CometChatIncomingCall /> (additive mode) goes inside the root navigator OR in the App.tsx wrapper, ABOVE all stacks/tabs. Same rule as web — calls only ring on screens where the listener exists.
In standalone mode, CallKit/ConnectionService own the foreground UI; <CometChatIncomingCall /> is not used. Instead, a react-native-callkeep event listener at app root reports new calls to the OS.
1.8 Three canonical provider/scaffold patterns (non-negotiable)
Validated across 4 RN cohorts on 2026-05-14. Each bug silently breaks integration in a different way; each fix is one line. Future scaffolds MUST emit all three.
1.8.a — getLoggedInUser() throws on no-session; always .catch(() => null).
// ❌ Throws "User not found" on every fresh launch → init fails → app stuck
const existing = await CometChatUIKit.getLoggedInUser();
if (existing) return;
// ✅
const existing = await CometChatUIKit.getLoggedInUser().catch(() => null);
if (existing) return;
The RN SDK treats "no logged-in user" as a thrown error (not null), unlike the web SDK. Without the catch, every fresh launch aborts before reaching login().
1.8.b — Render e.message, not String(e).
// ❌ Most CometChat SDK errors are plain objects, not Error subclasses
// → setError(String(e)) shows "[object Object]" on screen
catch (e) {
setError(String(e));
}
// ✅
catch (e) {
initialized = false; // let hot-reload retry
const msg = e instanceof Error ? e.message : JSON.stringify(e);
setError(msg);
}
Hiding the underlying error from the user is the single biggest debugging-time-sink in this stack. Always render e.message (or JSON.stringify(e) as fallback) so the actionable text reaches the screen.
1.8.c — DO NOT pass onAccept to <CometChatIncomingCall>.
// ❌ Short-circuits the kit's internal acceptCall + OngoingCall transition.
// Symptom: callee taps Accept; caller's outgoing screen stays on "Calling…"
// indefinitely; the call connects at server level but UI never transitions.
<CometChatIncomingCall call={call} onAccept={(c) => navigate('OngoingCall', ...)} ... />
// ✅ Let the kit own the accept path; only handle decline + error
<CometChatIncomingCall
call={call}
onDecline={() => setCallReceived(false)}
onError={() => setCallReceived(false)}
/>
The kit calls CometChat.acceptCall internally and pushes its own OngoingCall surface. Providing onAccept replaces that behavior with the caller's function — typically incomplete, never matches what the kit does.
Also recommended — guard creds at init time so undefined @env/process.env.EXPO_PUBLIC_* values surface as actionable errors:
if (!appId || !region) {
throw new Error(
`Missing CometChat credentials at init time: appId=${JSON.stringify(appId)}, region=${JSON.stringify(region)}. ` +
`Check .env defines COMETCHAT_APP_ID/COMETCHAT_REGION/COMETCHAT_AUTH_KEY ` +
`and restart Metro with cache wipe.`,
);
}
Without this guard, undefined env values produce opaque TypeError: undefined is not a function from deep inside the SDK — the most expensive failure mode of a typo'd env name.
2. Setup
Bare RN CLI
npm install @cometchat/chat-sdk-react-native @cometchat/calls-sdk-react-native
npm install react-native-callkeep react-native-voip-push-notification @react-native-firebase/app @react-native-firebase/messaging
npm install react-native-webrtc # Calls SDK peer dep
npm install react-native-gesture-handler react-native-reanimated # already installed if using UI Kit
iOS — four hardening steps before pod install (validated 2026-05-14 on Apple Silicon, iOS 26.5 sim):
-
USE_FRAMEWORKS=static pod install— required for WebRTC + CometChat pods. Default dynamic linkage silently produces a binary that can't load WebRTC at runtime. Set in shell rc or invoke every time:cd ios && USE_FRAMEWORKS=static pod install && cd .. # Subsequent builds: USE_FRAMEWORKS=static npx react-native run-ios -
Remove
EXCLUDED_ARCHS arm64 i386fromios/Podfile(Apple Silicon). RN templates often add this Intel-era workaround to the post-install hook; on Apple Silicon it blocksreact-native-webrtc/JitsiWebRTCarm64 slices from linking against the simulator. Delete the line:# ❌ Remove this from post_install on Apple Silicon config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' -
ios/Info.plist— minimum set for calls:<key>NSCameraUsageDescription</key> <string>Camera access for video calls</string> <key>NSMicrophoneUsageDescription</key> <string>Microphone access for voice and video calls</string> <key>NSBluetoothAlwaysUsageDescription</key> <string>Bluetooth access for using headsets during calls</string> <key>UIBackgroundModes</key> <array> <string>audio</string> <string>voip</string> <string>remote-notification</string> </array>UIBackgroundModes: audiois the load-bearing one for call-audio survival when the app backgrounds; without it, audio cuts the instant the app loses foreground.NSBluetoothAlwaysprevents a crash when the user connects a Bluetooth headset mid-call. -
ios/.xcode.env.localNODE_BINARY — point at a stable Homebrew Node path, not an nvm path that may have been cleaned up:# ios/.xcode.env.local export NODE_BINARY=/opt/homebrew/opt/node@20/bin/nodenvm paths in
~/.nvm/versions/node/v20.x.y/bin/nodego stale when nvm prunes — Xcode build fails withnode: command not found. Homebrew/opt/homebrew/opt/node@20/bin/nodeis symlinked to whatever node@20.x is currently installed; survivesbrew upgrade.
iOS-on-RN works even where iOS V5 NATIVE is blocked. The native iOS V5 cohort is gated upstream by a Cloudsmith 404 on cometchat-calls-ios. The RN-on-iOS path links a different WebRTC surface (react-native-webrtc + JitsiWebRTC pod transitives), so customers on RN + iOS are NOT blocked by the native cohort's vendor issue.
Android — manifest permissions (rule 1.3 + 1.6), Firebase config (google-services.json in android/app/), service registration:
<service
android:name="io.wazo.callkeep.RNCallKeepBackgroundMessagingService"
android:foregroundServiceType="phoneCall|microphone|camera"
android:exported="false" />
Expo managed
Calls SDK requires native modules — Expo managed CANNOT run it without a custom dev client. The skill detects the project mode:
- Managed + has
expo-dev-client: scaffolds config plugins for callkeep/firebase/voip-push, regenerates native projects, builds dev client - Managed without dev client: prompts the user — calls require either ejecting to bare or adding
expo-dev-client - EAS Build: configures
eas.jsonprofiles + build commands
Expo Go (the public dev client) cannot run calls. The skill states this clearly and refuses to scaffold without a dev client.
⚠️ Real build-time landmines on Expo SDK 54 + chat-uikit-react-native 5.3.x (validated 2026-05-14, 4 cohorts)
A previous version of this doc listed three "Expo SDK 54 build traps" (document-picker removal, react-native-worklets install, NDK override). Re-validation on 2026-05-14 across expo-new, expo-existing, rn-new, rn-existing showed none of those three fired on the current combo (chat-uikit-react-native@5.3.5 + calls-sdk-react-native@5.0.0/4.4.1 + Expo SDK 54). Removed. The real landmines on this combo are different:
-
@cometchat/calls-lib-webrtcis Cloudsmith-only, NOT on npm.npm install @cometchat/calls-lib-webrtcreturns 404. The package lives on CometChat's Cloudsmith registry. Use the tarball URL:npm install --legacy-peer-deps \ 'https://dl.cloudsmith.io/public/cometchat/cometchat/raw/files/cometchat-calls-lib-webrtc-346a46ff.tgz'The exact revision hash may roll forward — check the Cloudsmith page for the current version. Without this, runtime fails when the WebRTC layer initializes.
-
--legacy-peer-depssilently strips peer deps the kit needs at runtime. chat-uikit-react-native does not declare all its transitive runtime peers inpeerDependencies(kit + calls SDK together pull inexpo-linking,expo-constants,expo-asset,expo-fontvia expo-router, plusvalibot,zustand,@xmldom/xmldom,abab,promise.allsettled,text-encoding,react-native-url-polyfill,react-native-performance). With--legacy-peer-deps, npm skips them. Each one bites on first bundle asUnable to resolve module .... Reinstall them explicitly:# Bare RN npm install --legacy-peer-deps \ valibot zustand @xmldom/xmldom abab promise.allsettled text-encoding \ react-native-url-polyfill react-native-performance # Expo (resolves to SDK-compatible versions) npx expo install \ expo-linking expo-constants expo-asset expo-font \ -- --legacy-peer-deps -
expo.extra(app.json) caches on the Expo dev client manifest. Edits toapp.json → expo.extraafterexpo prebuilddo NOT reload to the device —Constants.expoConfig.extrakeeps reading the prebuild-time snapshot. For dev iteration on credentials, EITHER hardcode insrc/config/*.ts(Metro hot-bundles source changes) OR runexpo prebuild --clean && expo run:androidafter everyapp.json → extrachange. -
react-native startdoes NOT runadb reverse(bare RN only). Onlyreact-native run-androidsetsadb reverse tcp:8081 tcp:8081. When you restart Metro standalone (e.g. after.envchanges), the device loses port-forwarding and shows "unable to load scripts." Fix:adb reverse tcp:8081 tcp:8081 -
react-native-dotenvneeds Metro--reset-cacheafter.envchanges (bare RN only). The babel plugin processes.envat compile time, not runtime. Workflow:pkill -9 -f "react-native start" npx react-native start --reset-cache adb reverse tcp:8081 tcp:8081 adb shell am force-stop com.<package> && adb shell monkey -p com.<package> -c android.intent.category.LAUNCHER 1
Init
// cometchat/init.ts
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { CometChatCalls } from "@cometchat/calls-sdk-react-native";
let initialized = false;
export async function initCometChat() {
if (initialized) return;
const appSettings = new CometChat.AppSettingsBuilder()
.subscribePresenceForAllUsers()
.setRegion(process.env.EXPO_PUBLIC_COMETCHAT_REGION!)
.build();
await CometChat.init(process.env.EXPO_PUBLIC_COMETCHAT_APP_ID!, appSettings);
const callAppSettings = new CometChatCalls.CallAppSettingsBuilder()
.setAppId(process.env.EXPO_PUBLIC_COMETCHAT_APP_ID!)
.setRegion(process.env.EXPO_PUBLIC_COMETCHAT_REGION!)
.build();
CometChatCalls.init(callAppSettings);
initialized = true;
}
(Bare RN uses react-native-dotenv and @env imports instead of process.env.EXPO_PUBLIC_* — see cometchat-native-bare-patterns.)
3. Components catalog
Calls SDK primitives
Same names + shapes as the JavaScript SDK (Section 3 of cometchat-react-calls). The RN SDK adds platform-specific helpers — CometChatCalls.setUserVideoProxy for native track piping etc. — covered in deeper sample-app references.
UI Kit views (additive mode — @cometchat/chat-uikit-react-native)
| Component | Purpose |
|---|---|
<CometChatCallButtons user={u} group={g} /> | Voice + video icon row (typically inside CometChatMessageHeader). Group + user semantics differ — see callout below |
<CometChatIncomingCall /> | Root-mounted listener |
<CometChatOutgoingCall /> | Auto-mounted by IncomingCall on initiate |
<CometChatOngoingCall /> | Active call view |
<CometChatCallLogs onItemClick={fn} /> | History |
⚠️ Group calls use message-based join, not the ringing channel (validated 2026-05-15)
<CometChatCallButtons group={group} /> does NOT call CometChat.initiateCall like the 1:1 user variant does. Source: node_modules/@cometchat/chat-uikit-react-native/src/calls/CometChatCallButtons/CometChatCallButtons.tsx:138-201.
| Surface | What <CometChatCallButtons> does |
|---|---|
user={u} | CometChat.initiateCall(call) → standard Ringing flow → onIncomingCallReceived fires on peer's CallListener |
group={g} | CometChat.sendCustomMessage(meetingMessage) → meeting-card message in the group; caller jumps straight to in-call surface |
Implication for receivers:
-
Other kit-based clients (apps using
<CometChatMessageList />to render group messages) — get the "Join meeting" card rendered automatically; tap to join. No additional plumbing. -
Custom-UI clients (apps that render their own message list, or no list at all) — receive NOTHING on the
CallListenerchannel for group calls. To handle group meetings, add aCometChat.addMessageListenerand check for the custom meeting type:CometChat.addMessageListener('GROUP_MEETING_LISTENER', new CometChat.MessageListener({ onCustomMessageReceived: (msg) => { if (msg.getCategory() === CometChat.CATEGORY_CUSTOM && msg.getType() === 'meeting') { const sessionId = (msg.getCustomData() as any)?.sessionId; const callType = (msg.getCustomData() as any)?.callType; // "audio" | "video" // Show your own "incoming group call" UI; tap to navigate to ongoing-call with sessionId } }, }));
This semantic is the same across all CometChat kits (React, Angular, native iOS, native Android, Flutter) — group calls broadcast via custom message, NOT the ringing channel. Document loudly because the symptom (group-call recipient sees nothing) looks like a bug but is by design.
4. Standalone integration
When product === "voice-video" and there is no existing UI Kit.
Split by calling mode — these are two different shapes:
4a. Standalone — Session mode (meeting-room UX, no ringing)
Calls SDK ONLY. NO Chat SDK. Matches ~/Downloads/calls-sdk/calls-sdk-react-native-5/sample-apps/cometchat-calls-sample-app-react-native/.
MANDATORY install set (MUST run BEFORE scaffolding files — bundle will fail with Unable to resolve module <name> for each one missing):
# Bare RN — session-only mode (all version pins are load-bearing, see notes below)
npm install --legacy-peer-deps \
'@cometchat/calls-sdk-react-native@^5.0.0' \
'https://dl.cloudsmith.io/public/cometchat/cometchat/raw/files/cometchat-calls-lib-webrtc-346a46ff.tgz' \
'react-native-webrtc@^124.0.0' \
'react-native-permissions@^5.0.0' \
'react-native-safe-area-context@^5.0.0' \
'@react-native-async-storage/async-storage@^2.2.0' \
'@xmldom/xmldom@^0.8.11' \
'react-native-svg@^15.0.0' \
'react-native-background-timer@^2.4.1' \
'react-native-performance@^5.1.0' \
'react-native-url-polyfill@^2.0.0' \
'valibot@^1.2.0' \
'zustand@^5.0.0' \
'text-encoding@^0.7.0' \
'abab@^2.0.6' \
'promise.allsettled@^1.0.7'
# The Cloudsmith tarball sometimes silently skips on the same install line as
# other packages — re-run it on its own if it's missing afterwards:
[ -d node_modules/@cometchat/calls-lib-webrtc ] || \
npm install --save --force \
'https://dl.cloudsmith.io/public/cometchat/cometchat/raw/files/cometchat-calls-lib-webrtc-346a46ff.tgz'
Critical version pins — empirically validated 2026-05-15 (test #3, RN bare on Pixel 3 + RN 0.85):
| Pin | Why |
|---|---|
@react-native-async-storage/async-storage@^2.2.0 | v3.x splits Android native code into a separate Maven artifact (org.asyncstorage.shared_storage:storage-android:1.0.0) that isn't widely published. Gradle fails with Could not find org.asyncstorage.shared_storage:.... Pin to ^2.2.0 (self-contained Android). |
@xmldom/xmldom@^0.8.11 | The Calls SDK's polyfill (dist/polyfills/browser.js) assumes the xmldom 0.8 prototype shape. v0.9.x reorganized the prototype chain; runtime crashes with Cannot set property 'innerHTML' of undefined during polyfill init. Pin to ^0.8.11 (matches SDK's declared peer range). |
react-native-webrtc@^124.0.0 | Older versions don't support React Native 0.74+ Fabric. |
valibot@^1.2.0 / zustand@^5.0.0 | SDK declares these specific majors; minor upgrades have been API-stable but pin to the declared range to avoid surprises. |
Metro config patch: route the calls SDK to its .mjs entry. @cometchat/calls-sdk-react-native@5.0.0 ships a broken CJS bundle at dist/index.js — line 1 is import "./polyfills" (ESM syntax inside a CJS file). Metro can't parse it; the import returns undefined and crashes at runtime as Cannot read property 'CometChatCalls' of undefined in your init.ts. The dist/index.mjs is correctly formed. Patch metro.config.js:
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const path = require('path');
const config = {
resolver: {
resolveRequest: (context, moduleName, platform) => {
if (moduleName === '@cometchat/calls-sdk-react-native') {
return {
filePath: path.resolve(
__dirname,
'node_modules/@cometchat/calls-sdk-react-native/dist/index.mjs',
),
type: 'sourceFile',
};
}
return context.resolveRequest(context, moduleName, platform);
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
After patching, npx react-native start --reset-cache to clear Metro's resolver cache, then run-android / run-ios. Customer-found 2026-05-15 (test #3).
11 peer deps total — derived empirically (test #3, 2026-05-15) by parsing the SDK's actual bundle imports (node_modules/@cometchat/calls-sdk-react-native/dist/index.js + dist/polyfills/*.js). Every one is a real bundle-time or runtime requirement. The npm CLI --legacy-peer-deps flag silently strips them, so they must be listed explicitly.
Failure-mode breakdown:
| Missing peer | Symptom |
|---|---|
@react-native-async-storage/async-storage | Unable to resolve module @react-native-async-storage/async-storage at first bundle |
react-native-svg | Unable to resolve module react-native-svg at first bundle |
react-native-background-timer | Unable to resolve module react-native-background-timer (polyfills/browser.js) |
valibot, zustand, react-native-webrtc | Same — bundle-time imports in SDK index |
@xmldom/xmldom, abab, promise.allsettled | Same — polyfills/browser.js DOM + Promise polyfills |
text-encoding, react-native-url-polyfill, react-native-performance | Runtime — used inside SDK init code paths |
Also: DO NOT install @cometchat/chat-sdk-react-native in session-only mode. Dead weight in source (never imported, never initialized) AND it transitively pulls async-storage which conflicts with our own peer-dep management.
Verify checks: rn_webrtc_peers (11-peer presence) + no_chat_sdk_in_session_only (Chat SDK absence).
The skill then scaffolds:
cometchat/init.ts—CometChatCalls.init({ appId, region, authKey })ONLY. NoCometChat.init, noCometChat.login. PassauthKeyat init soCometChatCalls.login(uid)needs no second arg.screens/JoinSession.tsx— UID picker + Start/Join meeting + state machine (inMeeting && callToken).screens/CallRoom.tsx— Renders<CometChatCalls.Component callToken={callToken} />insideSafeAreaView.onConnectionClosedlistener resets state. Seereferences/call-session.md.- No VoIP push — session mode is link-driven, not ringing-driven. No
react-native-callkeep, no PushKit, no FCM data messages. - Native config — Camera + microphone permissions only.
Why no Chat SDK / no VoIP push: session mode never initiates a call entity. Customers tap a meeting link, the app generates a token, joins the session. There's nothing to "ring." Initializing both SDKs + the VoIP-push stack adds substantial complexity for zero benefit.
4b. Standalone — Ringing mode (CallKeep + CallKit/ConnectionService + Incoming/Outgoing/Ongoing kit)
Dual-SDK: Chat SDK signaling channel + Calls SDK media channel. The skill scaffolds:
cometchat/init.ts— Chat SDK + Calls SDK init (sequential).cometchat/CometChatProvider.tsx— Provider with init+login gate.hooks/useCallKeep.ts— Sets upreact-native-callkeep, registers event listeners (didReceiveStartCallAction, answerCall, endCall).services/voipPush.ts— Combinesreact-native-voip-push-notification(iOS) +@react-native-firebase/messaging(Android). Handles incoming-call payloads →RNCallKeep.displayIncomingCall(...).screens/ProfileScreen.tsx(or wherever the call trigger lives) — Voice + video buttons.screens/OngoingCallScreen.tsx— Hosts call surface via<CometChatCalls.Component />OR custom UI overlay. Implements rule 1.5 cleanup (CometChatCalls.leaveSession()+RNCallKeep.endCall(callUUID)).screens/CallLogsScreen.tsx— Paginated history.- Native config — Info.plist, AndroidManifest.xml, Firebase setup, capabilities.
- Server-side push docs — describes the VoIP cert + FCM key requirements; cannot automate.
5. Additive integration
When cometchat-native-core integration already exists. The skill:
- Adds
@cometchat/calls-sdk-react-native+ the four push deps. - Patches
cometchat/init.tsto addCometChatCalls.initafterCometChat.init. - Mounts
<CometChatIncomingCall />at app root (rule 1.7). - Wires
CometChatMessageHeadercall buttons (auto-rendered whenuserprop is set). - VoIP push: opt-in (asks user — substantial native config).
- Adds a
CallLogsScreento the existing navigator if the user picked "dedicated screen".
6. Anti-patterns
- Using Expo Go for calls. Calls require native modules that Expo Go can't load. Either eject or use a dev client. The skill refuses to scaffold against Expo Go.
- Skipping
cd ios && pod installafter install. Symbols missing → "Native module CometChatCalls is null" runtime error. Bare RN only. - Mounting
<CometChatIncomingCall />inside a stack/tab navigator. Loses the listener on stack push. Mount in App.tsx above the navigator (rule 1.7). - Forgetting
RNCallKeep.endCallin the hangup path. CallKit/ConnectionService thinks the call is still active; lock-screen UI gets stuck. - Sending Android push as
notificationinstead ofdata. ConnectionService cannot interceptnotificationpayloads. Server must senddata: { type: "incoming_call", sessionId: "..." }withpriority: "high". - Missing Firebase
google-services.jsonfor Android. Builds fail at compile time but the error is buried in Gradle output. - Mixing
react-native-callkeepv4 with RN <0.70. Older RN versions need callkeep v3.x; the skill checks RN version. - Passing
onAcceptto<CometChatIncomingCall>(rule 1.8.c). Short-circuits the kit's internalacceptCall+ OngoingCall transition — callee's UI moves but caller stays on "Calling…" indefinitely. Only handleonDecline+onError; let the kit own the accept path. setError(String(e))in catch blocks (rule 1.8.b). Most SDK errors aren'tErrorsubclasses → screen shows[object Object]. Usee instanceof Error ? e.message : JSON.stringify(e).await CometChatUIKit.getLoggedInUser()without.catch(() => null)(rule 1.8.a). RN SDK throws "User not found" on no-session — kills init before login can run.pod installwithoutUSE_FRAMEWORKS=static. Default dynamic linkage produces a binary that can't load WebRTC at runtime. iOS only.EXCLUDED_ARCHS = 'arm64 i386'in Podfile post_install on Apple Silicon. Intel-era workaround that breaks arm64 simulator linking for WebRTC pods. Remove on Apple Silicon hosts.react-native startfollowed by app reload withoutadb reverse tcp:8081 tcp:8081. Onlyrun-androidsets the port forward; standalone Metro restart loses it. Symptom: "unable to load scripts." Bare RN only.- Editing
.envand expecting Metro hot-reload to pick it up.react-native-dotenvis a babel-time plugin —.envchanges need Metro--reset-cache. Bare RN only. - Installing CometChat deps with
--legacy-peer-depsand expecting all transitive peers to land. npm silently skips peers — installvalibot,zustand,@xmldom/xmldom,abab,promise.allsettled,text-encoding,react-native-url-polyfill,react-native-performance, plus Expo'sexpo-linking/expo-constants/expo-asset/expo-fontexplicitly. See §2 setup landmines. npm install @cometchat/calls-lib-webrtc. Returns 404 — package is Cloudsmith-only. Use thehttps://dl.cloudsmith.io/...tarball URL.
7. Verification checklist
Static:
- All seven packages: chat-sdk-react-native, calls-sdk-react-native, callkeep, voip-push-notification, firebase/app, firebase/messaging, webrtc
-
@cometchat/calls-lib-webrtcinstalled via Cloudsmith tarball (not npm — npm returns 404) - iOS:
Info.plisthas UIBackgroundModes (audio + voip + remote-notification) + camera/mic/Bluetooth strings (rule 1.6 + §2 setup) - iOS: PushKit token registration in App.tsx (or a hook called from there)
- iOS:
USE_FRAMEWORKS=staticused forpod install - iOS (Apple Silicon): no
EXCLUDED_ARCHS = 'arm64 i386'in Podfile post_install - iOS:
.xcode.env.localNODE_BINARY points at stable Homebrew node path - Android: manifest has all four FOREGROUND_SERVICE_* permissions + MANAGE_OWN_CALLS + BIND_TELECOM_CONNECTION_SERVICE
- Android:
google-services.jsoninandroid/app/ - Android: callkeep service registered with correct
foregroundServiceType - CometChatIncomingCall mounted at App.tsx (additive mode), no
onAcceptprop (rule 1.8.c) - CometChatUIKit.getLoggedInUser called with
.catch(() => null)(rule 1.8.a) - Error rendering uses
e.messagenotString(e)(rule 1.8.b) - Env-var guard: provider throws actionable error when appId/region undefined (rule 1.8.c "Also recommended")
- Hangup path includes
endSession+RNCallKeep.endCall - Module-level
initializedflag
Runtime (real devices, both platforms):
- iOS — terminated app, lock screen rings on incoming call
- iOS — answer from lock screen → opens app, joins ongoing call
- Android — terminated app, ConnectionService rings (full-screen heads-up) on incoming call
- Android — answer from notification → opens app, joins ongoing call
- Both — outgoing call connects, two-way audio + video
- Both — hangup releases camera + mic, no lingering system call UI
- Both — Android 14+: ongoing-call notification visible, doesn't get killed by swipe
- Expo: only on dev client / standalone build, not Expo Go
8. Pointers
cometchat-native-core— provider, init, gesture handler peer depscometchat-native-{expo,bare}-patterns— pod install, gesture handler, dev client setupcometchat-native-components— full UI Kit catalog (additive mode)cometchat-native-push— APNs + FCM for chat (overlap with VoIP push but distinct paths — chat push is APNs/FCM standard, VoIP push is PushKit/FCM data-message)cometchat-native-production— server-minted tokenscometchat-native-troubleshooting— Metro cache, pod install failures, privacy manifest, gesture handler conflicts
Capabilities
Install
Quality
deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 27 github stars · SKILL.md body (34,169 chars)