<!-- AvatarFactory Docs · https://avatarfactory.in/docs/sdk/react-native/examples · Full map: https://avatarfactory.in/llms.txt -->

# Examples

Complete, working React Native code for every mode and the patterns you'll reach for most.

All examples authenticate with `getSessionToken`. Swap `fetchSessionToken` for your own backend call — see [AvatarProvider → Authentication](/docs/sdk/react-native/avatar-provider#authentication).

```tsx
// Shared helper used across the examples below.
async function fetchSessionToken() {
  const res = await fetch("https://your-backend.com/api/avatar-token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ avatarId: "default" }),
  });
  const { sessionToken } = await res.json();
  return sessionToken;
}
```

## Full call mode screen

A complete voice conversation screen with status tracking and event logging.

```tsx
import { View, Text } from "react-native";
import {
  AvatarProvider,
  Avatar,
  useAvatar,
  useAvatarEvent,
} from "@avatarfactory/react-native";

const config = {
  getSessionToken: fetchSessionToken,
  mode: "call" as const,
  avatar: {
    avatarId: "default",
    systemPrompt: "First-time visitor, arrived from the pricing page.",
  },
  controls: { enabled: true },
};

export function CallScreen() {
  return (
    <AvatarProvider config={config}>
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <Avatar style={{ width: 320, height: 320 }} />
        <StatusBar />
      </View>
    </AvatarProvider>
  );
}

function StatusBar() {
  const { isConnected, isSpeaking, isListening, error } = useAvatar();

  useAvatarEvent("start", () => console.log("[avatar] connected"));
  useAvatarEvent("error", (err) => console.error("[avatar]", err.code));

  return (
    <View style={{ flexDirection: "row", gap: 12, marginTop: 16 }}>
      {isConnected && <Text style={{ color: "#34d399" }}>Live ●</Text>}
      {isListening && <Text style={{ color: "#34d399" }}>Listening…</Text>}
      {isSpeaking && <Text style={{ color: "#60a5fa" }}>Speaking…</Text>}
      {error && <Text style={{ color: "#f87171" }}>Error: {error.message}</Text>}
    </View>
  );
}
```

## TTS mode with custom controls

Send text programmatically and manage sessions with your own UI.

```tsx
import { useState } from "react";
import { View, Button, TextInput } from "react-native";
import { AvatarProvider, Avatar, useAvatar } from "@avatarfactory/react-native";

const config = {
  getSessionToken: fetchSessionToken,
  mode: "tts" as const,
  avatar: { avatarId: "default" },
  controls: { enabled: false },
};

export function TtsScreen() {
  return (
    <AvatarProvider config={config}>
      <View style={{ flex: 1 }}>
        <Avatar style={{ width: 320, height: 320, alignSelf: "center" }} />
        <TtsControls />
      </View>
    </AvatarProvider>
  );
}

function TtsControls() {
  const { start, stop, speakText, interrupt, isReady, isSpeaking, isIdle } = useAvatar();
  const [text, setText] = useState("");

  return (
    <View style={{ padding: 16, gap: 12 }}>
      <Button
        title={isIdle ? "Connect" : "Disconnect"}
        onPress={isIdle ? start : stop}
      />

      <TextInput
        value={text}
        onChangeText={setText}
        placeholder="Type something for the avatar to say…"
        style={{ borderWidth: 1, borderColor: "#333", borderRadius: 8, padding: 10, color: "#fff" }}
        placeholderTextColor="rgba(255,255,255,0.4)"
      />

      <Button
        title="Speak"
        onPress={() => speakText?.(text)}
        disabled={!isReady || !text}
      />

      {isSpeaking && <Button title="Stop" onPress={interrupt} />}
    </View>
  );
}
```

## Player mode — onboarding screen

Zero server calls during playback. Pre-rendered audio plays locally with full lip-sync. (Auth is still required to load the avatar.)

```tsx
import { View, Button } from "react-native";
import { AvatarProvider, Avatar, useAvatar } from "@avatarfactory/react-native";

const welcomeJob = {
  audioChunks: ["...base64-encoded-audio..."],
  alignments: [
    {
      characters: ["W", "e", "l", "c", "o", "m", "e"],
      character_start_times_seconds: [0, 0.08, 0.16, 0.24, 0.32, 0.40, 0.48],
      character_end_times_seconds:   [0.08, 0.16, 0.24, 0.32, 0.40, 0.48, 0.56],
    },
  ],
};

const config = {
  getSessionToken: fetchSessionToken,
  mode: "player" as const,
  avatar: { avatarId: "default" },
  controls: { enabled: false },
};

export function OnboardingScreen() {
  return (
    <AvatarProvider config={config}>
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <Avatar style={{ width: 320, height: 320 }} />
        <PlayButton />
      </View>
    </AvatarProvider>
  );
}

function PlayButton() {
  const { play, isReady } = useAvatar();
  return (
    <Button title="Play Welcome" onPress={() => play?.(welcomeJob)} disabled={!isReady} />
  );
}
```

## Error handling with auto-reconnect

Robust pattern covering mic denial, timeout, and unexpected drops.

```tsx
import { View, Text, Button, Linking } from "react-native";
import { useAvatar, useAvatarEvent } from "@avatarfactory/react-native";

// Copy for the codes your users can act on. Everything else gets one generic
// line — error.message is written for you, not for them.
const COPY: Partial<Record<AvatarErrorCode, string>> = {
  MIC_PERMISSION_DENIED: "Enable your microphone in Settings.",
  SESSION_IDLE_TIMEOUT: "Session timed out due to inactivity.",
  CONNECTION_FAILED: "We couldn't reach the service. Check your connection.",
};

function RobustController() {
  const { error, start } = useAvatar();

  useAvatarEvent("error", (err) => {
    analytics.track("avatar_error", {
      code: err.code,
      source: err.source,
      fatal: err.fatal,
      message: err.message,
    });

  });

  // No branch replacing the avatar: a fatal error ends the session but leaves
  // the avatar painted with isIdle back to true, so the start button reappears
  // and IS the retry. Say what happened, plus a shortcut for the one fatal code
  // start() can't fix.
  if (error?.fatal) {
    return (
      <View>
        <Text>{COPY[error.code] ?? "Something went wrong on our side."}</Text>
        {!error.retryable && (
          <Button title="Open Settings" onPress={() => Linking.openSettings()} />
        )}
      </View>
    );
  }

  return null;
}
```

## Live context — in-app assistant

Feed the avatar real-time information about what the user is viewing.

```tsx
import { useEffect } from "react";
import { useAvatar } from "@avatarfactory/react-native";

function ProductAssistant({ product }) {
  const { addLiveContext } = useAvatar();

  // Update the AI's context whenever the product changes.
  useEffect(() => {
    addLiveContext?.({
      context:
        `User is viewing: ${product.name} — $${product.price}. ` +
        `Description: ${product.description}`,
      // Set update: true to replace the previous context entirely.
    });
  }, [product, addLiveContext]);

  return null; // Avatar UI is handled by <Avatar /> elsewhere
}
```

> `addLiveContext` defaults to **append** — each call adds to the existing context. Pass `update: true` to replace it. Keep context strings concise and factual; max 2000 characters.

## Audio mode — custom TTS pipeline

Use your own audio source (ElevenLabs, Play.ai, etc.) with the avatar's lip-sync. The audio must be raw PCM (16 kHz, mono).

```tsx
import { View, Button } from "react-native";
import { AvatarProvider, Avatar, useAvatar } from "@avatarfactory/react-native";

const config = {
  getSessionToken: fetchSessionToken,
  mode: "audio" as const,
  avatar: { avatarId: "default" },
  controls: { enabled: false },
};

export function AudioScreen() {
  return (
    <AvatarProvider config={config}>
      <View style={{ flex: 1 }}>
        <Avatar style={{ width: 320, height: 320, alignSelf: "center" }} />
        <AudioControls />
      </View>
    </AvatarProvider>
  );
}

function AudioControls() {
  const { speakAudio, start, stop, isReady, isIdle } = useAvatar();

  const playClip = async () => {
    const buffer = await loadAudioBuffer(); // raw PCM audio, 16 kHz mono
    speakAudio?.(buffer);
  };

  return (
    <View style={{ padding: 16, gap: 12 }}>
      <Button title={isIdle ? "Connect" : "Disconnect"} onPress={isIdle ? start : stop} />
      <Button title="Play clip" onPress={playClip} disabled={!isReady} />
    </View>
  );
}
```
