Skip to main content
This guide walks you through integrating the CometChat Calls SDK into an Ionic application. By the end, you’ll have a working video call implementation with proper authentication and lifecycle handling. This guide covers Ionic with Angular, React, and Vue.
For native mobile features like CallKit, VoIP push notifications, and background handling, consider using the native iOS or Android SDKs.

Prerequisites

Before you begin, ensure you have:
  • A CometChat account with an app created (Sign up)
  • Your App ID, Region, and API Key from the CometChat Dashboard
  • An Ionic project (Angular, React, or Vue)
  • Node.js 16+ installed
  • Ionic CLI installed (npm install -g @ionic/cli)

Step 1: Install the SDK

Install the CometChat Calls SDK package:
npm install @cometchat/calls-sdk-javascript

Ionic Angular

Step 2: Create the Service

Create a service that handles SDK initialization, authentication, and call operations. The service waits for the Ionic platform to be ready before initializing:
// src/app/services/cometchat-calls.service.ts
import { Injectable } from "@angular/core";
import { CometChatCalls } from "@cometchat/calls-sdk-javascript";
import { Platform } from "@ionic/angular";
import { BehaviorSubject } from "rxjs";

interface User {
  uid: string;
  name: string;
  avatar?: string;
}

@Injectable({
  providedIn: "root",
})
export class CometChatCallsService {
  private initialized = false;
  private _isReady$ = new BehaviorSubject<boolean>(false);
  private _user$ = new BehaviorSubject<User | null>(null);
  private _error$ = new BehaviorSubject<string | null>(null);

  isReady$ = this._isReady$.asObservable();
  user$ = this._user$.asObservable();
  error$ = this._error$.asObservable();

  // Replace with your CometChat credentials
  private readonly APP_ID = "YOUR_APP_ID";
  private readonly REGION = "YOUR_REGION";
  private readonly API_KEY = "YOUR_API_KEY";

  constructor(private platform: Platform) {}

  async initAndLogin(uid: string): Promise<boolean> {
    try {
      // Wait for Ionic platform to be ready
      await this.platform.ready();

      if (this.initialized) {
        return true;
      }

      // Step 1: Initialize the SDK
      const initResult = await CometChatCalls.init({
        appId: this.APP_ID,
        region: this.REGION,
      });

      if (!initResult.success) {
        throw new Error("SDK initialization failed");
      }

      // Step 2: Check if already logged in
      let loggedInUser = CometChatCalls.getLoggedInUser();

      // Step 3: Login if not already logged in
      if (!loggedInUser) {
        loggedInUser = await CometChatCalls.login(uid, this.API_KEY);
      }

      this.initialized = true;
      this._user$.next(loggedInUser);
      this._isReady$.next(true);
      return true;
    } catch (err: any) {
      console.error("CometChat Calls setup failed:", err);
      this._error$.next(err.message || "Setup failed");
      return false;
    }
  }

  getLoggedInUser(): User | null {
    return this._user$.value;
  }

  async generateToken(sessionId: string) {
    return CometChatCalls.generateToken(sessionId);
  }

  async joinSession(token: string, settings: any, container: HTMLElement) {
    return CometChatCalls.joinSession(token, settings, container);
  }

  leaveSession() {
    CometChatCalls.leaveSession();
  }

  muteAudio() {
    CometChatCalls.muteAudio();
  }

  unMuteAudio() {
    CometChatCalls.unMuteAudio();
  }

  pauseVideo() {
    CometChatCalls.pauseVideo();
  }

  resumeVideo() {
    CometChatCalls.resumeVideo();
  }

  addEventListener(event: string, callback: Function) {
    return CometChatCalls.addEventListener(event as any, callback as any);
  }
}

Step 3: Initialize in App Component

Initialize the SDK and login when the app starts:
// src/app/app.component.ts
import { Component, OnInit } from "@angular/core";
import { CometChatCallsService } from "./services/cometchat-calls.service";

@Component({
  selector: "app-root",
  templateUrl: "app.component.html",
})
export class AppComponent implements OnInit {
  constructor(private callsService: CometChatCallsService) {}

  ngOnInit() {
    // In a real app, get this from your authentication system
    const currentUserId = "cometchat-uid-1";
    this.callsService.initAndLogin(currentUserId);
  }
}

Step 4: Create the Call Page

Create a call page component that handles joining sessions, media controls, and cleanup:
// src/app/pages/call/call.page.ts
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { NavController } from "@ionic/angular";
import { CometChatCallsService } from "../../services/cometchat-calls.service";
import { Subscription } from "rxjs";

@Component({
  selector: "app-call",
  templateUrl: "./call.page.html",
  styleUrls: ["./call.page.scss"],
})
export class CallPage implements OnInit, OnDestroy {
  @ViewChild("callContainer", { static: true }) callContainer!: ElementRef;

  sessionId: string = "";
  isReady = false;
  isJoined = false;
  isJoining = false;
  isMuted = false;
  isVideoOff = false;
  error: string | null = null;
  
  private unsubscribers: Function[] = [];
  private subscriptions: Subscription[] = [];

  constructor(
    private route: ActivatedRoute,
    private navCtrl: NavController,
    private callsService: CometChatCallsService
  ) {}

  ngOnInit() {
    this.sessionId = this.route.snapshot.paramMap.get("sessionId") || "";
    
    // Subscribe to ready state
    this.subscriptions.push(
      this.callsService.isReady$.subscribe((ready) => {
        this.isReady = ready;
        if (ready && this.sessionId) {
          this.joinCall();
        }
      }),
      this.callsService.error$.subscribe((err) => {
        this.error = err;
      })
    );
  }

  ngOnDestroy() {
    this.cleanup();
    this.subscriptions.forEach((sub) => sub.unsubscribe());
  }

  private async joinCall() {
    if (!this.callContainer?.nativeElement) return;
    
    this.isJoining = true;
    this.error = null;

    try {
      // Register event listeners before joining
      this.unsubscribers = [
        this.callsService.addEventListener("onSessionJoined", () => {
          this.isJoined = true;
          this.isJoining = false;
        }),
        this.callsService.addEventListener("onSessionLeft", () => {
          this.isJoined = false;
          this.navCtrl.back();
        }),
        this.callsService.addEventListener("onAudioMuted", () => {
          this.isMuted = true;
        }),
        this.callsService.addEventListener("onAudioUnMuted", () => {
          this.isMuted = false;
        }),
        this.callsService.addEventListener("onVideoPaused", () => {
          this.isVideoOff = true;
        }),
        this.callsService.addEventListener("onVideoResumed", () => {
          this.isVideoOff = false;
        }),
      ];

      // Generate a call token for this session
      const tokenResult = await this.callsService.generateToken(this.sessionId);

      // Join the call session
      await this.callsService.joinSession(
        tokenResult.token,
        {
          sessionType: "VIDEO",
          layout: "TILE",
          startAudioMuted: false,
          startVideoPaused: false,
        },
        this.callContainer.nativeElement
      );
    } catch (err: any) {
      console.error("Failed to join call:", err);
      this.error = err.message || "Failed to join call";
      this.isJoining = false;
    }
  }

  toggleAudio() {
    this.isMuted ? this.callsService.unMuteAudio() : this.callsService.muteAudio();
  }

  toggleVideo() {
    this.isVideoOff ? this.callsService.resumeVideo() : this.callsService.pauseVideo();
  }

  leaveCall() {
    this.callsService.leaveSession();
  }

  private cleanup() {
    this.unsubscribers.forEach((unsub) => unsub());
    this.unsubscribers = [];
    this.callsService.leaveSession();
  }
}

Step 5: Create the Call Page Template

Create the HTML template for the call page with video container and controls:
<!-- src/app/pages/call/call.page.html -->
<ion-content>
  <!-- Loading state -->
  <div *ngIf="!isReady" class="loading-container">
    <ion-spinner></ion-spinner>
    <p>Initializing...</p>
  </div>

  <!-- Error state -->
  <div *ngIf="error" class="error-container">
    <ion-icon name="alert-circle" color="danger"></ion-icon>
    <p>{{ error }}</p>
    <ion-button (click)="joinCall()">Retry</ion-button>
  </div>

  <!-- Video container - SDK renders the call UI here -->
  <div #callContainer class="call-container" *ngIf="isReady && !error"></div>

  <!-- Joining overlay -->
  <div *ngIf="isJoining" class="joining-overlay">
    <ion-spinner></ion-spinner>
    <p>Joining call...</p>
  </div>

  <!-- Call controls -->
  <div class="call-controls" *ngIf="isJoined">
    <ion-button 
      (click)="toggleAudio()" 
      [color]="isMuted ? 'danger' : 'primary'"
      shape="round"
    >
      <ion-icon slot="icon-only" [name]="isMuted ? 'mic-off' : 'mic'"></ion-icon>
    </ion-button>
    
    <ion-button 
      (click)="toggleVideo()" 
      [color]="isVideoOff ? 'danger' : 'primary'"
      shape="round"
    >
      <ion-icon slot="icon-only" [name]="isVideoOff ? 'videocam-off' : 'videocam'"></ion-icon>
    </ion-button>
    
    <ion-button 
      (click)="leaveCall()" 
      color="danger"
      shape="round"
    >
      <ion-icon slot="icon-only" name="call"></ion-icon>
    </ion-button>
  </div>
</ion-content>
/* src/app/pages/call/call.page.scss */
.call-container {
  width: 100%;
  height: calc(100% - 80px);
  background-color: #1a1a1a;
}

.call-controls {
  display: flex;
  justify-content: center;
  gap: 16px;
  padding: 16px;
  background-color: #f5f5f5;
}

.loading-container,
.error-container,
.joining-overlay {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  gap: 16px;
}

.joining-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  z-index: 100;
}

Step 6: Create the Home Page

Create a home page where users can enter a session ID and join a call:
// src/app/pages/home/home.page.ts
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { CometChatCallsService } from "../../services/cometchat-calls.service";

@Component({
  selector: "app-home",
  templateUrl: "./home.page.html",
})
export class HomePage {
  sessionId = "";
  isReady$ = this.callsService.isReady$;
  user$ = this.callsService.user$;
  error$ = this.callsService.error$;

  constructor(
    private router: Router,
    private callsService: CometChatCallsService
  ) {}

  joinCall() {
    if (this.sessionId) {
      this.router.navigate(["/call", this.sessionId]);
    }
  }
}
<!-- src/app/pages/home/home.page.html -->
<ion-header>
  <ion-toolbar>
    <ion-title>CometChat Calls</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <div *ngIf="error$ | async as error" class="error-message">
    <ion-text color="danger">{{ error }}</ion-text>
  </div>

  <div *ngIf="!(isReady$ | async)" class="loading">
    <ion-spinner></ion-spinner>
    <p>Loading...</p>
  </div>

  <div *ngIf="isReady$ | async">
    <p *ngIf="user$ | async as user">
      Logged in as: {{ user.name || user.uid }}
    </p>

    <ion-item>
      <ion-label position="floating">Session ID</ion-label>
      <ion-input [(ngModel)]="sessionId" placeholder="Enter Session ID"></ion-input>
    </ion-item>

    <ion-button 
      expand="block" 
      (click)="joinCall()" 
      [disabled]="!sessionId"
      class="ion-margin-top"
    >
      Join Call
    </ion-button>
  </div>
</ion-content>

Ionic React

Step 2: Create the Provider

Create a context provider that handles SDK initialization and authentication:
// src/providers/CometChatCallsProvider.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { CometChatCalls } from "@cometchat/calls-sdk-javascript";
import { isPlatform } from "@ionic/react";

interface User {
  uid: string;
  name: string;
  avatar?: string;
}

interface CometChatCallsContextType {
  isReady: boolean;
  user: User | null;
  error: string | null;
}

const CometChatCallsContext = createContext<CometChatCallsContextType>({
  isReady: false,
  user: null,
  error: null,
});

// Replace with your CometChat credentials
const APP_ID = "YOUR_APP_ID";
const REGION = "YOUR_REGION";
const API_KEY = "YOUR_API_KEY";

interface ProviderProps {
  children: ReactNode;
  uid: string;
}

export function CometChatCallsProvider({ children, uid }: ProviderProps) {
  const [isReady, setIsReady] = useState(false);
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function initAndLogin() {
      try {
        // Step 1: Initialize the SDK
        const initResult = await CometChatCalls.init({
          appId: APP_ID,
          region: REGION,
        });

        if (!initResult.success) {
          throw new Error("SDK initialization failed");
        }

        // Step 2: Check if already logged in
        let loggedInUser = CometChatCalls.getLoggedInUser();

        // Step 3: Login if not already logged in
        if (!loggedInUser) {
          loggedInUser = await CometChatCalls.login(uid, API_KEY);
        }

        setUser(loggedInUser);
        setIsReady(true);
      } catch (err: any) {
        console.error("CometChat Calls setup failed:", err);
        setError(err.message || "Setup failed");
      }
    }

    if (uid) {
      initAndLogin();
    }
  }, [uid]);

  return (
    <CometChatCallsContext.Provider value={{ isReady, user, error }}>
      {children}
    </CometChatCallsContext.Provider>
  );
}

export function useCometChatCalls(): CometChatCallsContextType {
  return useContext(CometChatCallsContext);
}

Step 3: Wrap Your App

Add the provider to your app’s root component:
// src/App.tsx
import { IonApp, IonRouterOutlet, setupIonicReact } from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { Route } from "react-router-dom";
import { CometChatCallsProvider } from "./providers/CometChatCallsProvider";
import HomePage from "./pages/Home";
import CallPage from "./pages/Call";

setupIonicReact();

const App: React.FC = () => {
  // In a real app, get this from your authentication system
  const currentUserId = "cometchat-uid-1";

  return (
    <IonApp>
      <CometChatCallsProvider uid={currentUserId}>
        <IonReactRouter>
          <IonRouterOutlet>
            <Route exact path="/" component={HomePage} />
            <Route exact path="/call/:sessionId" component={CallPage} />
          </IonRouterOutlet>
        </IonReactRouter>
      </CometChatCallsProvider>
    </IonApp>
  );
};

export default App;

Step 4: Create the Call Page

Create a call page that handles joining sessions, media controls, and cleanup:
// src/pages/Call.tsx
import { useEffect, useRef, useState } from "react";
import { 
  IonContent, 
  IonPage, 
  IonButton, 
  IonIcon, 
  IonSpinner,
  useIonRouter 
} from "@ionic/react";
import { mic, micOff, videocam, videocamOff, call } from "ionicons/icons";
import { useParams } from "react-router-dom";
import { CometChatCalls } from "@cometchat/calls-sdk-javascript";
import { useCometChatCalls } from "../providers/CometChatCallsProvider";

const CallPage: React.FC = () => {
  const { sessionId } = useParams<{ sessionId: string }>();
  const { isReady, error: initError } = useCometChatCalls();
  const router = useIonRouter();
  const containerRef = useRef<HTMLDivElement>(null);
  
  // Call state
  const [isJoined, setIsJoined] = useState(false);
  const [isJoining, setIsJoining] = useState(false);
  const [isMuted, setIsMuted] = useState(false);
  const [isVideoOff, setIsVideoOff] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const unsubscribersRef = useRef<Function[]>([]);

  useEffect(() => {
    if (!isReady || !containerRef.current || !sessionId) return;

    async function joinCall() {
      setIsJoining(true);
      setError(null);

      try {
        // Register event listeners before joining
        unsubscribersRef.current = [
          CometChatCalls.addEventListener("onSessionJoined", () => {
            setIsJoined(true);
            setIsJoining(false);
          }),
          CometChatCalls.addEventListener("onSessionLeft", () => {
            setIsJoined(false);
            router.goBack();
          }),
          CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)),
          CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)),
          CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)),
          CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)),
        ];

        // Generate a call token for this session
        const tokenResult = await CometChatCalls.generateToken(sessionId);

        // Join the call session
        await CometChatCalls.joinSession(
          tokenResult.token,
          {
            sessionType: "VIDEO",
            layout: "TILE",
            startAudioMuted: false,
            startVideoPaused: false,
          },
          containerRef.current!
        );
      } catch (err: any) {
        console.error("Failed to join call:", err);
        setError(err.message || "Failed to join call");
        setIsJoining(false);
      }
    }

    joinCall();

    return () => {
      unsubscribersRef.current.forEach((unsub) => unsub());
      unsubscribersRef.current = [];
      CometChatCalls.leaveSession();
    };
  }, [isReady, sessionId, router]);

  // Control handlers
  const toggleAudio = () => {
    isMuted ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio();
  };

  const toggleVideo = () => {
    isVideoOff ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo();
  };

  const leaveCall = () => {
    CometChatCalls.leaveSession();
  };

  // Loading state
  if (!isReady) {
    return (
      <IonPage>
        <IonContent className="ion-padding ion-text-center">
          <IonSpinner />
          <p>Initializing...</p>
        </IonContent>
      </IonPage>
    );
  }

  // Error state
  if (error || initError) {
    return (
      <IonPage>
        <IonContent className="ion-padding ion-text-center">
          <p style={{ color: "var(--ion-color-danger)" }}>
            Error: {error || initError}
          </p>
          <IonButton onClick={() => window.location.reload()}>Retry</IonButton>
        </IonContent>
      </IonPage>
    );
  }

  return (
    <IonPage>
      <IonContent>
        {/* Video container - SDK renders the call UI here */}
        <div 
          ref={containerRef} 
          style={{ 
            width: "100%", 
            height: "calc(100% - 80px)", 
            backgroundColor: "#1a1a1a" 
          }} 
        />

        {/* Joining overlay */}
        {isJoining && (
          <div style={{
            position: "absolute",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            justifyContent: "center",
            backgroundColor: "rgba(0, 0, 0, 0.7)",
            color: "white",
            zIndex: 100,
          }}>
            <IonSpinner color="light" />
            <p>Joining call...</p>
          </div>
        )}

        {/* Call controls */}
        {isJoined && (
          <div style={{ 
            display: "flex", 
            justifyContent: "center", 
            gap: "16px", 
            padding: "16px" 
          }}>
            <IonButton 
              onClick={toggleAudio}
              color={isMuted ? "danger" : "primary"}
              shape="round"
            >
              <IonIcon slot="icon-only" icon={isMuted ? micOff : mic} />
            </IonButton>
            <IonButton 
              onClick={toggleVideo}
              color={isVideoOff ? "danger" : "primary"}
              shape="round"
            >
              <IonIcon slot="icon-only" icon={isVideoOff ? videocamOff : videocam} />
            </IonButton>
            <IonButton 
              onClick={leaveCall}
              color="danger"
              shape="round"
            >
              <IonIcon slot="icon-only" icon={call} />
            </IonButton>
          </div>
        )}
      </IonContent>
    </IonPage>
  );
};

export default CallPage;

Step 5: Create the Home Page

Create a home page where users can enter a session ID and join a call:
// src/pages/Home.tsx
import { useState } from "react";
import { 
  IonContent, 
  IonPage, 
  IonHeader, 
  IonToolbar, 
  IonTitle,
  IonItem,
  IonLabel,
  IonInput,
  IonButton,
  IonSpinner,
  IonText,
  useIonRouter
} from "@ionic/react";
import { useCometChatCalls } from "../providers/CometChatCallsProvider";

const HomePage: React.FC = () => {
  const { isReady, user, error } = useCometChatCalls();
  const router = useIonRouter();
  const [sessionId, setSessionId] = useState("");

  const joinCall = () => {
    if (sessionId) {
      router.push(`/call/${sessionId}`);
    }
  };

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>CometChat Calls</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent className="ion-padding">
        {error && (
          <IonText color="danger">
            <p>{error}</p>
          </IonText>
        )}

        {!isReady ? (
          <div className="ion-text-center">
            <IonSpinner />
            <p>Loading...</p>
          </div>
        ) : (
          <>
            <p>Logged in as: {user?.name || user?.uid}</p>

            <IonItem>
              <IonLabel position="floating">Session ID</IonLabel>
              <IonInput
                value={sessionId}
                onIonChange={(e) => setSessionId(e.detail.value || "")}
                placeholder="Enter Session ID"
              />
            </IonItem>

            <IonButton
              expand="block"
              onClick={joinCall}
              disabled={!sessionId}
              className="ion-margin-top"
            >
              Join Call
            </IonButton>
          </>
        )}
      </IonContent>
    </IonPage>
  );
};

export default HomePage;

Ionic Vue

Step 2: Create the Composable

Create a composable that handles SDK initialization and authentication:
// src/composables/useCometChatCalls.ts
import { ref, readonly } from "vue";
import { CometChatCalls } from "@cometchat/calls-sdk-javascript";

interface User {
  uid: string;
  name: string;
  avatar?: string;
}

// Replace with your CometChat credentials
const APP_ID = "YOUR_APP_ID";
const REGION = "YOUR_REGION";
const API_KEY = "YOUR_API_KEY";

// Shared state across all components
const isReady = ref(false);
const user = ref<User | null>(null);
const error = ref<string | null>(null);
const initialized = ref(false);

export function useCometChatCalls() {
  async function initAndLogin(uid: string): Promise<boolean> {
    if (initialized.value) {
      return isReady.value;
    }

    try {
      // Step 1: Initialize the SDK
      const initResult = await CometChatCalls.init({
        appId: APP_ID,
        region: REGION,
      });

      if (!initResult.success) {
        throw new Error("SDK initialization failed");
      }

      // Step 2: Check if already logged in
      let loggedInUser = CometChatCalls.getLoggedInUser();

      // Step 3: Login if not already logged in
      if (!loggedInUser) {
        loggedInUser = await CometChatCalls.login(uid, API_KEY);
      }

      user.value = loggedInUser;
      isReady.value = true;
      initialized.value = true;
      return true;
    } catch (err: any) {
      console.error("CometChat Calls setup failed:", err);
      error.value = err.message || "Setup failed";
      return false;
    }
  }

  return {
    isReady: readonly(isReady),
    user: readonly(user),
    error: readonly(error),
    initAndLogin,
  };
}

Step 3: Initialize in App Component

Initialize the SDK and login when the app starts:
<!-- src/App.vue -->
<template>
  <ion-app>
    <ion-router-outlet />
  </ion-app>
</template>

<script setup lang="ts">
import { IonApp, IonRouterOutlet } from "@ionic/vue";
import { onMounted } from "vue";
import { useCometChatCalls } from "./composables/useCometChatCalls";

const { initAndLogin } = useCometChatCalls();

onMounted(() => {
  // In a real app, get this from your authentication system
  const currentUserId = "cometchat-uid-1";
  initAndLogin(currentUserId);
});
</script>

Step 4: Create the Call Page

Create a call page that handles joining sessions, media controls, and cleanup:
<!-- src/views/CallPage.vue -->
<template>
  <ion-page>
    <ion-content>
      <!-- Loading state -->
      <div v-if="!isReady" class="loading-container">
        <ion-spinner></ion-spinner>
        <p>Initializing...</p>
      </div>

      <!-- Error state -->
      <div v-else-if="callError" class="error-container">
        <ion-text color="danger">{{ callError }}</ion-text>
        <ion-button @click="joinCall">Retry</ion-button>
      </div>

      <!-- Video container - SDK renders the call UI here -->
      <div v-else ref="callContainer" class="call-container"></div>

      <!-- Joining overlay -->
      <div v-if="isJoining" class="joining-overlay">
        <ion-spinner color="light"></ion-spinner>
        <p>Joining call...</p>
      </div>

      <!-- Call controls -->
      <div v-if="isJoined" class="call-controls">
        <ion-button 
          @click="toggleAudio" 
          :color="isMuted ? 'danger' : 'primary'"
          shape="round"
        >
          <ion-icon slot="icon-only" :icon="isMuted ? micOff : mic"></ion-icon>
        </ion-button>
        
        <ion-button 
          @click="toggleVideo" 
          :color="isVideoOff ? 'danger' : 'primary'"
          shape="round"
        >
          <ion-icon slot="icon-only" :icon="isVideoOff ? videocamOff : videocam"></ion-icon>
        </ion-button>
        
        <ion-button 
          @click="leaveCall" 
          color="danger"
          shape="round"
        >
          <ion-icon slot="icon-only" :icon="call"></ion-icon>
        </ion-button>
      </div>
    </ion-content>
  </ion-page>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { 
  IonPage, 
  IonContent, 
  IonButton, 
  IonIcon, 
  IonSpinner,
  IonText
} from "@ionic/vue";
import { mic, micOff, videocam, videocamOff, call } from "ionicons/icons";
import { CometChatCalls } from "@cometchat/calls-sdk-javascript";
import { useCometChatCalls } from "../composables/useCometChatCalls";

const route = useRoute();
const router = useRouter();
const sessionId = route.params.sessionId as string;

const { isReady } = useCometChatCalls();

// Template refs
const callContainer = ref<HTMLDivElement | null>(null);

// Call state
const isJoined = ref(false);
const isJoining = ref(false);
const isMuted = ref(false);
const isVideoOff = ref(false);
const callError = ref<string | null>(null);

// Store unsubscribe functions for cleanup
const unsubscribers = ref<Function[]>([]);

async function joinCall() {
  if (!callContainer.value || !sessionId) return;

  isJoining.value = true;
  callError.value = null;

  try {
    // Register event listeners before joining
    unsubscribers.value = [
      CometChatCalls.addEventListener("onSessionJoined", () => {
        isJoined.value = true;
        isJoining.value = false;
      }),
      CometChatCalls.addEventListener("onSessionLeft", () => {
        isJoined.value = false;
        router.back();
      }),
      CometChatCalls.addEventListener("onAudioMuted", () => {
        isMuted.value = true;
      }),
      CometChatCalls.addEventListener("onAudioUnMuted", () => {
        isMuted.value = false;
      }),
      CometChatCalls.addEventListener("onVideoPaused", () => {
        isVideoOff.value = true;
      }),
      CometChatCalls.addEventListener("onVideoResumed", () => {
        isVideoOff.value = false;
      }),
    ];

    // Generate a call token for this session
    const tokenResult = await CometChatCalls.generateToken(sessionId);

    // Join the call session
    await CometChatCalls.joinSession(
      tokenResult.token,
      {
        sessionType: "VIDEO",
        layout: "TILE",
        startAudioMuted: false,
        startVideoPaused: false,
      },
      callContainer.value
    );
  } catch (err: any) {
    console.error("Failed to join call:", err);
    callError.value = err.message || "Failed to join call";
    isJoining.value = false;
  }
}

function toggleAudio() {
  isMuted.value ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio();
}

function toggleVideo() {
  isVideoOff.value ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo();
}

function leaveCall() {
  CometChatCalls.leaveSession();
}

function cleanup() {
  unsubscribers.value.forEach((unsub) => unsub());
  unsubscribers.value = [];
  CometChatCalls.leaveSession();
}

// Watch for SDK ready state and join when ready
watch(isReady, (ready) => {
  if (ready && callContainer.value) {
    joinCall();
  }
});

onMounted(() => {
  if (isReady.value && callContainer.value) {
    joinCall();
  }
});

onUnmounted(() => {
  cleanup();
});
</script>

<style scoped>
.call-container {
  width: 100%;
  height: calc(100% - 80px);
  background-color: #1a1a1a;
}

.call-controls {
  display: flex;
  justify-content: center;
  gap: 16px;
  padding: 16px;
}

.loading-container,
.error-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  gap: 16px;
}

.joining-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  z-index: 100;
}
</style>

Step 5: Create the Home Page

Create a home page where users can enter a session ID and join a call:
<!-- src/views/HomePage.vue -->
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>CometChat Calls</ion-title>
      </ion-toolbar>
    </ion-header>
    
    <ion-content class="ion-padding">
      <ion-text v-if="error" color="danger">
        <p>{{ error }}</p>
      </ion-text>

      <div v-if="!isReady" class="ion-text-center">
        <ion-spinner></ion-spinner>
        <p>Loading...</p>
      </div>

      <template v-else>
        <p>Logged in as: {{ user?.name || user?.uid }}</p>

        <ion-item>
          <ion-label position="floating">Session ID</ion-label>
          <ion-input 
            v-model="sessionId" 
            placeholder="Enter Session ID"
          ></ion-input>
        </ion-item>

        <ion-button 
          expand="block" 
          @click="joinCall" 
          :disabled="!sessionId"
          class="ion-margin-top"
        >
          Join Call
        </ion-button>
      </template>
    </ion-content>
  </ion-page>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { 
  IonPage, 
  IonHeader, 
  IonToolbar, 
  IonTitle, 
  IonContent,
  IonItem,
  IonLabel,
  IonInput,
  IonButton,
  IonSpinner,
  IonText
} from "@ionic/vue";
import { useCometChatCalls } from "../composables/useCometChatCalls";

const router = useRouter();
const { isReady, user, error } = useCometChatCalls();

const sessionId = ref("");

function joinCall() {
  if (sessionId.value) {
    router.push(`/call/${sessionId.value}`);
  }
}
</script>

Step 6: Configure Routes

Set up the router with the home and call pages:
// src/router/index.ts
import { createRouter, createWebHistory } from "@ionic/vue-router";
import { RouteRecordRaw } from "vue-router";
import HomePage from "../views/HomePage.vue";
import CallPage from "../views/CallPage.vue";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    component: HomePage,
  },
  {
    path: "/call/:sessionId",
    component: CallPage,
  },
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
});

export default router;
For more detailed information on specific topics covered in this guide, refer to the main documentation: