Cookbooks · In your product

A call screen in a React Native app

A screen in an Expo or React Native app that puts the person holding the phone on a call with your agent, with mute and hang up. The server side is the same as for a web page; only the client changes.

Before you start

  • An agent, and its ID. The Quickstart makes one.
  • A workspace API key in the CRACKLE_API_KEY environment variable. See Authentication.
  • A route on your backend that starts a call and returns livekitUrl and token. The one in A talk button for your website will do.
  • An Expo app you can make a development build of. Calls use native audio code, so they do not run in Expo Go.
  1. Install LiveKit's React Native packages

    Terminal
    npx expo install @livekit/react-native @livekit/react-native-webrtc livekit-client \
      @livekit/react-native-expo-plugin @config-plugins/react-native-webrtc expo-keep-awake

    Add the two config plugins and say why the app wants the microphone. The plugins do the native setup when the app is built. LiveKit’s own guide at docs.livekit.io covers a bare React Native project, where that setup is done by hand.

    app.json
    {
      "expo": {
        "plugins": ["@livekit/react-native-expo-plugin", "@config-plugins/react-native-webrtc"],
        "ios": {
          "infoPlist": {
            "NSMicrophoneUsageDescription": "Allow the microphone so you can talk on a call."
          }
        },
        "android": {
          "permissions": ["android.permission.RECORD_AUDIO"]
        }
      }
    }
    Terminal
    npx expo prebuild
    npx expo run:ios   # or run:android
  2. Register the WebRTC globals

    This has to run once, before anything creates a room, so it goes in the app’s entry file.

    index.js
    import { registerGlobals } from '@livekit/react-native';
    import { registerRootComponent } from 'expo';
    import App from './App';
    
    // Once, before any room is created: this gives React Native the WebRTC pieces a browser has built in.
    registerGlobals();
    
    registerRootComponent(App);
  3. Build the call screen

    The screen starts the phone’s audio session, asks your backend for a call, and hands the result to LiveKitRoom, which connects, publishes the microphone and plays the agent. There is no audio element to attach, as there is in a browser.

    CallScreen.jsx
    import { useEffect, useState } from 'react';
    import { Button, Text, View } from 'react-native';
    import { AudioSession, LiveKitRoom, useRoomContext } from '@livekit/react-native';
    import { useKeepAwake } from 'expo-keep-awake';
    
    export default function CallScreen({ onDone }) {
      const [session, setSession] = useState(null);
      const [error, setError] = useState('');
      useKeepAwake(); // a call that stops when the screen dims is a dropped call
    
      useEffect(() => {
        let cancelled = false;
        (async () => {
          try {
            await AudioSession.startAudioSession();
            // Your own backend, which calls POST /v1/calls with the API key.
            const response = await fetch('https://your-app.com/api/call', { method: 'POST' });
            if (!response.ok) throw new Error('The call could not be started.');
            const body = await response.json();
            if (!cancelled) setSession(body);
          } catch (err) {
            if (!cancelled) setError(err.message);
          }
        })();
        return () => {
          cancelled = true;
          AudioSession.stopAudioSession();
        };
      }, []);
    
      if (error) return <Text>{error}</Text>;
      if (!session) return <Text>Connecting…</Text>;
    
      return (
        <LiveKitRoom
          serverUrl={session.livekitUrl}
          token={session.token}
          connect
          audio
          video={false}
          onDisconnected={onDone}
        >
          <InCall />
        </LiveKitRoom>
      );
    }
    
    function InCall() {
      const room = useRoomContext();
      const [muted, setMuted] = useState(false);
    
      const toggleMute = async () => {
        await room.localParticipant.setMicrophoneEnabled(muted);
        setMuted(!muted);
      };
    
      return (
        <View>
          <Button title={muted ? 'Unmute' : 'Mute'} onPress={toggleMute} />
          <Button title="Hang up" onPress={() => room.disconnect()} />
        </View>
      );
    }

Try it

Run the development build on a real phone if you can: that is where the microphone, the earpiece and the lock screen behave as they will for the people using your app. Open the screen, allow the microphone, and the agent greets you. Hanging up disconnects the room, which ends the call with the reason user-left.

What a phone adds

SituationWhat to do
The screen locks or dims mid-callKeep it awake for the length of the call, as useKeepAwake does above.
The app goes to the background on AndroidRun a foreground service of the microphone type for the call, or Android stops the microphone. Google Play asks you to declare it.
The app goes to the background on iOSTurn on the audio background mode for the app.
The person picks the earpiece or the speakerChoose the output with AudioSession.selectAudioOutput.
The network changes or dropsonDisconnected fires. Show that the call ended and offer to call again.

Live captions and the agent’s state arrive the same way as on the web, on the goblins-transcript topic: see Web and mobile clients. React Native does not always include TextDecoder; if yours does not, add a polyfill before decoding the messages.