cometchat-react-calls

SkillDev tools

CometChat Calls SDK integration for web React apps (Vite, CRA, Next.js, React Router, Astro). Covers @cometchat/calls-sdk-javascript install, dual-SDK init (Chat SDK + Calls SDK), generateToken (v4's getRTCToken was removed in v5), the kit's CometChatIncomingCall / CometChatOutgoingCall / CometChatOngoingCall components, CallButtons composition, getUserMedia permissions, browser TURN/STUN handling, and additive-vs-standalone modes.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the cometchat-react-calls skill

What this skill tells your AI

The instructions your AI receives, as published by cometchat/cometchat-skills in skills/cometchat-react-calls/SKILL.md and read by ahel’s review.

Ground truth: @cometchat/chat-uikit-react@^6 (+ @cometchat/calls-sdk-javascript@^5) — installed package types + ui-kit/react. Official docs: https://www.cometchat.com/docs/calls/javascript/overview · Docs MCP: claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp (or fetch the URL directly without MCP). Verify symbols against the installed package/source before relying on them.

⚠️ STOP — mandatory precondition before any code

Before writing one line of code, you MUST resolve mode = ringing | session. This decides which entire integration shape you scaffold — they don't share UI, navigation, or surface.

modeSurface shapeReference
ringingCometChatIncomingCall at root + CometChatCallButtons near a user / contact / message header. Recipient's screen rings on incoming call.references/ringing-integration.md
session/meet/:sessionId route (or equivalent) + CometChatCalls.joinSession on a container <div>. No ringing — both parties enter the same session ID.references/call-session.md

How to resolve mode (in order):

  1. Check .cometchat/config.json for mode — if set by the cometchat-calls dispatcher's Step 3.0, trust it.

  2. Infer from the user's words — see cometchat-calls/SKILL.md Step 3.0 inference table. Confirm in one line: "Got it — setting up Ringing. Say so if you wanted meeting-room URLs instead."

  3. If still ambiguous (e.g. user said only "integrate calls" with no qualifier) — ASK before scaffolding. Don't default to ringing. Use this prompt verbatim — preserve the order, the labels, and the descriptions exactly. Do NOT rephrase. Do NOT swap options. Option 1 is "Session"; option 2 is "Ringing":

    • question: "What kind of calling experience are you building?"
    • header: "Calling mode"
    • multiSelect: false
    • options (display in this exact order — Session FIRST, Ringing SECOND):
      1. label: "Session — meeting / conference room", description: "Multiple users join the same session by ID or link. No ringing. Like Google Meet, Zoom, or a Slack huddle."
      2. label: "Ringing — 1:1 or group calls", description: "One user calls another (or a small group). Recipient's device rings; they accept or decline. Like FaceTime or WhatsApp calls."

    Strict-order rule: the agent's UI primitive must render option 1 above the option 2. Do not let auto-mode classifiers or your own bias reorder them. Session is shown first because the dashboard team has standardized on this order across CometChat product surfaces; consistency matters more than any subjective "first option" preference.

Do not write CometChatCallButtons / CometChatIncomingCall / CometChatOngoingCall code without a confirmed mode === "ringing". Those components are the kit's implementation of ringing; using them silently locks the integration into ringing-shape even if the user wanted a meeting-room flow.


⚠️ Call container — must have non-zero dimensions when joinSession fires

The Calls SDK measures the container <div> synchronously when joinSession runs and throws Container dimensions and number of tiles must be positive if width or height is 0. This is the calls equivalent of the chat-layout flex-shrink trap.

Common bug — h-full on a flex child resolves to 0:

// ✗ WRONG — `h-full` is `height: 100%`, but the parent's height is auto
//   so 100% of auto = 0. SDK crashes.
<section className="flex-1">
  <div ref={containerRef} className="h-full w-full" />
</section>

Fix — use a flex chain with min-h-0 and an explicit fallback:

// ✓ RIGHT — section is a flex column with min-h-0 (so it can shrink), the
//   container uses flex-1 to claim remaining space, plus an explicit
//   `minHeight` safety net for very short viewports.
<section className="relative flex flex-1 min-h-0">
  <div
    ref={containerRef}
    className="flex-1 w-full"
    style={{ minHeight: 400 }}
  />
</section>

If you can't use flex (e.g. fixed-height modal), just give the container explicit pixels:

<div
  ref={containerRef}
  style={{ width: "100%", height: "calc(100vh - 100px)" }}
/>

minHeight: 0 matters specifically for parent flex containers that house the call container — without it, a flex-column ancestor whose content overflows defaults to min-height: auto and the call surface gets squeezed to zero. This is the same trap as cometchat-react-patterns's chat-layout rule, applied to calls.


⚠️ Idle timeout is in MILLISECONDS (the "Are you still there? → instant exit" bug)

The idle-timeout values are milliseconds, not seconds. This is the single most common calls-config footgun (customer-reported 2026-06): setting idleTimeoutPeriodBeforePrompt: 180 thinking "180 seconds" means 180 ms — so the moment you join a session alone, the "Are you still there?" prompt fires and the call exits in a fraction of a second.

// ✗ WRONG — read as 180ms / 30ms → prompt + exit almost instantly on join
const settings = { idleTimeoutPeriodBeforePrompt: 180, idleTimeoutPeriodAfterPrompt: 30 };

// ✓ RIGHT — milliseconds. (defaults: 60_000 / 120_000)
const settings = {
  idleTimeoutPeriodBeforePrompt: 180_000, // 180s before the prompt
  idleTimeoutPeriodAfterPrompt: 60_000,   // 60s grace before disconnect
};

Set them as SessionSettings object fields (used with joinSession(token, settings, container)): { idleTimeoutPeriodBeforePrompt: 180_000, idleTimeoutPeriodAfterPrompt: 60_000 }. ⚠️ There are no setIdleTimeoutPeriodBeforePrompt/AfterPrompt builder methods — CallSettingsBuilder has only a single setIdleTimeoutPeriod(ms); the before/after split exists only as the two object fields. idleTimeoutPeriodAfterPrompt has a 60_000 ms (60s) minimum — smaller values are silently clamped to 60s. To effectively disable it, use a huge value (86_400_000 = 24h), never 0 or a tiny number. The timer only counts down when you're the only participant — so a single-person session/test triggers it fastest. Full recipe (prompt UI + extend/leave): references/idle-timeout.md.


⚠️ Next.js / SSR — mandatory bundler config

Both SDKs ship code that breaks Next.js's SSR pass:

  • @cometchat/chat-sdk-javascript references window at module load time
  • @cometchat/calls-sdk-javascript (v5) imports Node built-ins (fs, path) gated by a runtime check that the bundler still tries to statically resolve

"use client" alone does NOT fix this — Next.js evaluates client components during the initial SSR pass for hydration. You need to defer the SDK imports so they only execute in the browser.

For Next.js (App Router or Pages Router), apply ALL of these:

  1. Switch dev/build to webpack in package.json scripts (Turbopack's fs/path aliasing is fragile in Next 16):

    {
      "scripts": {
        "dev": "next dev --webpack",
        "build": "next build --webpack",
        "start": "next start"
      }
    }
    
  2. Add a webpack fs fallback in next.config.ts:

    import type { NextConfig } from "next";
    const nextConfig: NextConfig = {
      webpack: (config, { isServer }) => {
        if (!isServer) {
          config.resolve = config.resolve || {};
          config.resolve.fallback = {
            ...(config.resolve.fallback || {}),
            fs: false,
            path: false,
          };
        }
        return config;
      },
    };
    export default nextConfig;
    
  3. Wrap the CometChatProvider in next/dynamic({ ssr: false }) — create a small client wrapper and use it from a server-component layout:

    // app/_components/CometChatGate.tsx
    "use client";
    import dynamic from "next/dynamic";
    import type { ReactNode } from "react";
    
    const CometChatProvider = dynamic(
      () => import("@/cometchat/CometChatProvider").then((m) => m.CometChatProvider),
      { ssr: false, loading: () => <div>Loading…</div> },
    );
    
    export function CometChatGate({ children }: { children: ReactNode }) {
      return <CometChatProvider>{children}</CometChatProvider>;
    }
    

    ⚠️ Provider placement for Ringing mode — mount at app root, not on a sub-route layout. If the CometChatProvider registers a global CallListener for incoming calls (which it should — see "Ringing mode listener" below), it MUST be mounted in the root layout (app/layout.tsx). Mounting it on a sub-route layout like app/meet/layout.tsx means the listener is only armed while the user is browsing under that sub-route — incoming calls land silently when they're on the home page or any other route, and the caller sees a timeout/rejection.

    // app/layout.tsx (server component, root layout)
    import { CometChatGate } from "./_components/CometChatGate";
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html><body>
          <CometChatGate>{children}</CometChatGate>
        </body></html>
      );
    }
    

    For Session-only mode (no ringing — both parties navigate to a shared /meet/:id URL), the sub-route layout is fine — listener isn't load-bearing.

    Ringing mode listener — inside CometChatProvider, after login:

    const { CometChat } = await import("@cometchat/chat-sdk-javascript");
    CometChat.addCallListener("ringing-listener", new CometChat.CallListener({
      onIncomingCallReceived: async (call: any) => {
        const accepted = await CometChat.acceptCall(call.getSessionId());
        router.push(`/meet/${encodeURIComponent(accepted.getSessionId())}`);
      },
      onOutgoingCallAccepted: (call: any) => {
        router.push(`/meet/${encodeURIComponent(call.getSessionId())}`);
      },
      onOutgoingCallRejected: (call: any) => { /* show toast */ },
      onIncomingCallCancelled: (call: any) => { /* dismiss any UI */ },
    }));
    

    The /meet/:sessionId page handles joinSession. Validated end-to-end against Pixel 3 V6 Android peer on 2026-05-12.

  4. Lazy-load the SDKs inside init.ts — replace top-level static imports with await import(...) inside the init/login functions. The provider is gated by step 3, but init.ts is shared with any page that uses CometChatCalls directly, so belt-and-braces it:

    let chatModule: typeof import("@cometchat/chat-sdk-javascript") | null = null;
    let callsModule: typeof import("@cometchat/calls-sdk-javascript") | null = null;
    
    async function loadSdks() {
      if (!chatModule) chatModule = await import("@cometchat/chat-sdk-javascript");
      if (!callsModule) callsModule = await import("@cometchat/calls-sdk-javascript");
      return { CometChat: chatModule.CometChat, CometChatCalls: callsModule.CometChatCalls };
    }
    
  5. No top-level SDK imports in any page that's reachable via App Router routing. Inside useEffect handlers, dynamic-import the SDK:

    useEffect(() => {
      let CallsSdk: typeof import("@cometchat/calls-sdk-javascript").CometChatCalls | null = null;
      (async () => {
        const mod = await import("@cometchat/calls-sdk-javascript");
        CallsSdk = mod.CometChatCalls;
        // ... use CallsSdk
      })();
      return () => {
        try { CallsSdk?.leaveSession(); } catch { /* noop */ }
      };
    }, []);
    

Skipping any of these reproduces the failure mode: the route 500s with either Module not found: Can't resolve 'fs' or ReferenceError: window is not defined. Verified empirically against Next.js 16.2.6 + Calls SDK 5.0.0-beta.2.

For Vite / React Router / Astro, none of this applies — those bundlers don't pre-evaluate client modules.


Purpose

Production-grade voice + video calling for React-family web apps. Loaded by cometchat-calls when framework is one of reactjs, nextjs, react-router, or astro. Operates in two modes:

  • Standalone — calls is the product. @cometchat/chat-sdk-javascript (signaling) + @cometchat/calls-sdk-javascript (WebRTC) + a small set of UI Kit call components. No CometChatConversations / CometChatMessageList / etc.
  • Additive — calls layered onto an existing CometChat React UI Kit integration. Adds call buttons inline, mounts CometChatIncomingCall at app root.

Read these other skills first:

  • cometchat-calls — dispatcher (modes, hard rules, anti-patterns)
  • cometchat-core — Chat SDK init, login, env-var prefix per framework, SSR safety
  • Framework-specific patterns: cometchat-react-patterns / cometchat-nextjs-patterns / cometchat-react-router-patterns / cometchat-astro-patterns

Ground truth:

When to use

  • React-family web apps with voice/video calling: Vite + React, Next.js (App or Pages Router), React Router v6/v7, Astro with React islands.
  • The user wants 1:1 OR group calls AND wants the calls UI surface (not pure server-side / signaling-only).
  • Either calling mode applies — ringing (kit-driven incoming call screen) OR session (meeting-room URL pattern).

When NOT to use

  • Chat-only integrations (no calling at all) — skip this skill entirely. Load only cometchat-core + cometchat-react-patterns + cometchat-components. Don't install @cometchat/calls-sdk-javascript — the calls SDK alone is ~700 KB; with the kit + chat SDK the full production bundle of a chat-and-calls app is ~4.8 MB JS (verified — real vite build). That's expected; a green build also emits benign COMMONJS_VARIABLE_IN_ESM warnings from the calls SDK's own code and a >500 KB chunk-size warning — these are NOT failures.
  • Native mobile (Android / iOS / RN / Flutter) — load the cohort-specific calls skill (cometchat-native-calls, cometchat-android-v6-calls, cometchat-ios-calls, cometchat-flutter-v6-calls). The Calls SDK is platform-specific; APIs and lifecycle differ.
  • Angular — load cometchat-angular-calls. Same underlying JS Calls SDK but wrapped in Angular Services + @Output() event bindings (NOT React-style callback props — verified runtime smoke 2026-06-02 caught the v4→v5 binding inversion).
  • SDK-only (no UI Kit)cometchat-react-calls §4c covers the SDK-only path in detail; this is the right skill, but you're using a specific subset. Don't import <CometChatCallButtons> / <CometChatOngoingCall> / <CometChatIncomingCall> from @cometchat/chat-uikit-react in that mode.
  • Server-side token-mint server work — load cometchat-production for the REST-API token recipes; this skill is client-side.
  • Visual Builder calls — load cometchat-core §11 + the framework-specific patterns; the Visual Builder generates calls wiring differently.

1. The seven hard rules — web specialization

1.1 Dual-SDK contract

@cometchat/chat-sdk-javascript for ringing; @cometchat/calls-sdk-javascript for the WebRTC session. They are separate npm packages.

// ✓ RIGHT — initiate ringing (Chat SDK)
import { CometChat } from "@cometchat/chat-sdk-javascript";

const outgoing = new CometChat.Call(receiverUid, CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER);
const initiated = await CometChat.initiateCall(outgoing);
// initiated.getSessionId() — the ID the Calls SDK will join
// ✓ RIGHT — join WebRTC session (Calls SDK v5)
import { CometChatCalls } from "@cometchat/calls-sdk-javascript";

// v5 — plain SessionSettings object, no Builder.
// `as const` keeps the string literals narrow ("VIDEO"/"TILE") so they satisfy
// the SDK's SessionType / Layout unions — a bare object widens them to `string`.
const sessionSettings = {
  sessionType: "VIDEO",   // or "VOICE"
  layout: "TILE",
} as const;

// v5 — generateToken takes ONLY sessionId (Calls SDK has its own auth state
// after CometChatCalls.login(); no authToken arg needed).
const tokenRes = await CometChatCalls.generateToken(sessionId);

// htmlElement is REQUIRED — pass the DOM container the SDK should draw into
const container = document.getElementById("ongoing-call-root")!;
const result = await CometChatCalls.joinSession(tokenRes.token, sessionSettings, container);
if (result?.error) {
  console.error("joinSession failed:", result.error);
}

The two-Call-classes problem from Android does NOT exist on JS — there's only one CometChat.Call constructor. But the dual-SDK split still trips up agents trained on the chat-only docs.

1.2 VoIP push — N/A on web (browsers don't have VoIP push)

The mandatory-VoIP-push rule from mobile families does not apply to web. Browsers cannot ring a closed tab. The standalone-mode equivalent is Web Push notifications (Service Worker + Notification API + push subscriptions) — useful for nudging the user to a tab where the call screen is open, but they do not bypass tab/page-load.

The skill scaffolds Web Push as an opt-in (asks user); it is not strictly required. Production calls UX on web typically pairs with email/SMS fallback for missed calls, not VoIP.

1.3 Lifecycle — getUserMedia cleanup

Web's equivalent of Android's foreground-service correctness is MediaStream track cleanup. Browsers don't release the camera/mic until tracks are explicitly stopped. The kit handles this for <CometChatOngoingCall />, but custom WebRTC surfaces (Section 4) must do:

function endCall() {
  // 1. End the Calls SDK session — releases the kit's internal stream
  CometChatCalls.leaveSession();   // v5 — was endSession() in v4 (still works as a deprecated shim)

  // 2. If you grabbed a custom MediaStream (preview, screen-share), stop tracks
  customStream?.getTracks().forEach(t => t.stop());
  customStream = null;

  // 3. Detach video elements
  if (videoEl.current) videoEl.current.srcObject = null;
}

Skipping this leaves the camera light on until the tab is closed. Same canonical bug as iOS rule 1.5.

1.4 Calls login — the DEFAULT path needs NONE; only directCalling/SDK-only do

For the common case — additive ringing / default calling — do NOT call CometChatCalls.login. Install the calls SDK, let the call buttons appear automatically in CometChatMessageHeader, and mount <CometChatIncomingCall /> once at the app root. That's the whole wiring. Both canonical React v6 sample apps do calls this exact way with ZERO CometChatCalls.login/CometChatCalls.init (verified: cometchat-uikit-react-v6/sample-app/src/components/CometChatHome/CometChatHome.tsx:1740 mounts only <CometChatIncomingCall />; no CometChatCalls.login anywhere in either sample). The kit's default defaultCalling mode rides the Chat SDK's signaling — adding a calls-login step here is needless plumbing, and an empty/wrong arg makes it silently no-op.

CometChatCalls.login is required ONLY for (a) CallWorkflow.directCalling (conference-style 1:1) or (b) the SDK-only / custom-WebRTC surface (§4c). In those cases — and only those — the v5 Calls SDK needs its own login: after CometChat.login() resolves on the chat side, call CometChatCalls.login(uid, apiKey) for dev or CometChatCalls.loginWithAuthToken(authToken) for production. The auth token is the same one your backend mints via the CometChat Create-Auth-Token API; the Calls SDK and Chat SDK accept it interchangeably.

// Dev
await CometChatCalls.login(uid, import.meta.env.VITE_COMETCHAT_API_KEY);

// Production (server-minted token)
await CometChatCalls.loginWithAuthToken(authTokenFromBackend);

About VITE_COMETCHAT_API_KEY: CometChatCalls.login(uid, apiKey) takes the app's Auth Key — the same value cometchat-core writes as VITE_COMETCHAT_AUTH_KEY (the env-prefix table establishes APP_ID/REGION/AUTH_KEY, not a separate API_KEY). In dev you can reuse VITE_COMETCHAT_AUTH_KEY here; if you prefer the _API_KEY name for readability, add it to your .env with the same Auth Key value. Don't leave it undefined — an empty arg makes the Calls login silently no-op (see the directCalling trap above).

cometchat-production (web) covers the token-endpoint pattern.

⚠️ CallWorkflow.directCalling silently fails without this login. When you opt 1:1 calls into the conference-style UI by passing callWorkflow={CallWorkflow.directCalling} to <CometChatCallButtons> / <CometChatOngoingCall> etc., the kit routes through the Calls SDK directly and requires CometChatCalls.login() to have completed. Without it, calls ring for ~2 seconds then drop with no error message — the most painful failure mode in this skill. The UI Kit's default defaultCalling mode does NOT have this requirement (it uses the Chat SDK's signaling). Rule when emitting directCalling: always include the CometChatCalls.login(...) call alongside the Chat login above (ENG-35709).

1.5 Hangup cleanup — see rule 1.3

1.6 Permissions — getUserMedia prompts

The browser handles the runtime permission prompt automatically when the Calls SDK calls getUserMedia. The integration must:

  • Surface a try/catch around startSession to handle NotAllowedError (user denied)
  • Surface NotFoundError (no camera/mic on device — common on desktops with no webcam)
  • Render a clear in-app explanation BEFORE the browser prompts, so users know what they're agreeing to (browsers ignore this in autoplay/iframe contexts but it improves grant rates)

There are no manifest-level permission declarations on web. HTTPS is required — the skill detects localhost (allowed) vs other origins (must be HTTPS) and warns if the dev server is HTTP.

1.7 IncomingCall mounted at app root

<CometChatIncomingCall /> (additive mode) or a Service-Worker-driven web-push handler (standalone mode) must mount above the route boundary so calls fire on every page.

// app/layout.tsx (Next.js App Router) or App.tsx (Vite/CRA)
<CometChatProvider>
  <CometChatIncomingCall />   {/* renders nothing when no call active; listens app-wide */}
  <Routes>...</Routes>
</CometChatProvider>

Mounting it inside a route component means it disappears on navigation — calls only ring on the screen where it's mounted. That's the canonical "calls don't work" bug on web.

1.8 Init order — Chat init → Chat login → Calls init → Calls login (ENG-35708)

The order is load-bearing. Two crashes from real testers trace back to this:

  • CometChatCalls.init before CometChat.login → calls integration broke. The Calls SDK reads context from the Chat SDK that only exists once a Chat session is established. Swapping init order silently fails or returns 401s on the first generateToken.
  • Crash on Start Call when only the Calls SDK was initialized. No Chat SDK init at all → the ringing flow can't fire initiateCall because the Chat SDK isn't there.

The only correct order in additive mode (chat + calls):

CometChat.init(appId, settings)
  → CometChat.login(uid, authKey)        // OR loginWithAuthToken(token)
  → CometChatCalls.init({appId, region})
  → CometChatCalls.login(uid, apiKey)    // OR loginWithAuthToken(serverToken)

In standalone session-mode (product === "voice-video", no chat), use ONLY the Calls SDK — never call CometChat.init / CometChat.login at all. The kit's session-mode sample doesn't import the Chat SDK; matching that shape eliminates a class of "Chat init failed mid-meeting" failures.

1.9 Don't double-up call buttons (ENG-35708)

Two testers reported call buttons appearing twice on the message screen. Cause: the kit's <CometChatMessageHeader user={user} /> already renders <CometChatCallButtons> internally when a user prop is set (and the kit's default messages page mounts the header). Adding your own <CometChatCallButtons user={user} /> next to either of those produces a duplicate set.

Rule before emitting <CometChatCallButtons>:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
105
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
cometchat-react-calls
Source
github.com/cometchat/cometchat-skills