> ## Documentation Index
> Fetch the complete documentation index at: https://cometchat-22654f5b-docs-rn-guide-message-privately.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Ionic Integration

> Integrate CometChat Calls SDK v5 in Ionic apps with package setup, initialization, permissions, and call UI rendering.

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.

<Note>
  For native mobile features like CallKit, VoIP push notifications, and background handling, consider using the native [iOS](/calls/ios/overview) or [Android](/calls/android/overview) SDKs.
</Note>

## Prerequisites

Before you begin, ensure you have:

* A CometChat account with an app created ([Sign up](https://app.cometchat.com/signup))
* 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:

```bash theme={null}
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:

```typescript theme={null}
// 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:

```typescript theme={null}
// 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:

```typescript theme={null}
// 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:

```html theme={null}
<!-- 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>
```

```scss theme={null}
/* 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:

```typescript theme={null}
// 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]);
    }
  }
}
```

```html theme={null}
<!-- 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:

```tsx theme={null}
// 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:

```tsx theme={null}
// 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:

```tsx theme={null}
// 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:

```tsx theme={null}
// 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:

```typescript theme={null}
// 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:

```vue theme={null}
<!-- 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:

```vue theme={null}
<!-- 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:

```vue theme={null}
<!-- 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:

```typescript theme={null}
// 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;
```

## Related Documentation

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

* [Setup](/calls/javascript/setup) - Detailed SDK installation and initialization
* [Authentication](/calls/javascript/authentication) - Login methods and user management
* [Session Settings](/calls/javascript/session-settings) - All available call configuration options
* [Join Session](/calls/javascript/join-session) - Session joining and token generation
* [Events](/calls/javascript/events) - Complete list of event listeners
* [Actions](/calls/javascript/actions) - All available call control methods
* [Call Layouts](/calls/javascript/call-layouts) - Layout options and customization
* [Participant Management](/calls/javascript/participant-management) - Managing call participants
