# @ringg/react-native

> ⚠️ **Alpha / pre-release.** Expect rough edges and breaking changes between
> versions. Pin an exact version if you need stability, and please report
> anything you hit. See **[Known issues](#known-issues)** below.

Embeddable chat + voice-call widget for [Ringg AI](https://ringg.ai) agents —
the React Native implementation. Drop `<RinggWidget />` over your app for text
chat, voice calls, interactive components (forms, calendars, quick replies,
Block Kit) and a post-call feedback screen.

It runs on the same headless brain as the web widget (`@ringg/core`, bundled
into this package), so conversation behaviour — reconnection, typing timing,
message ordering, optimistic sends — is identical across platforms. Only the
views are native.

## Install

```bash
npm install @ringg/react-native
```

Then the peer dependencies, which must be installed in **your** app so there is
exactly one autolinked copy of each native module:

```bash
npx expo install @livekit/react-native @livekit/react-native-webrtc livekit-client react-native-svg
# or, without Expo:
npm install @livekit/react-native @livekit/react-native-webrtc livekit-client react-native-svg
```

Requires React ≥ 18 · React Native ≥ 0.73.

**Expo Go will not work** — LiveKit ships native code, so you need a
[development build](https://docs.expo.dev/develop/development-builds/introduction/).

## Platform setup (required for voice)

**Expo** — add the LiveKit plugin and the permissions to `app.json`:

```json
{
  "expo": {
    "plugins": ["@livekit/react-native-expo-plugin"],
    "ios": {
      "infoPlist": {
        "NSMicrophoneUsageDescription": "Voice calls use the microphone.",
        "UIBackgroundModes": ["audio"]
      }
    },
    "android": {
      "permissions": [
        "android.permission.RECORD_AUDIO",
        "android.permission.MODIFY_AUDIO_SETTINGS",
        "android.permission.ACCESS_NETWORK_STATE",
        "android.permission.BLUETOOTH_CONNECT"
      ]
    }
  }
}
```

**Bare React Native** — the same keys, by hand: `NSMicrophoneUsageDescription`
and `UIBackgroundModes: [audio]` in `ios/<App>/Info.plist`, and the four
permissions above in `android/app/src/main/AndroidManifest.xml`.

## Integrate in 3 steps

### 1 · Register the WebRTC globals

Once, at your app entry, **before** anything imports LiveKit:

```ts
// index.js
import { registerGlobals } from "@livekit/react-native";
registerGlobals();
```

### 2 · Build a transport and a controller

```tsx
import { useEffect, useMemo } from "react";
import {
  RinggWidget,
  createLiveKitTransport,
  createNativeMicPermission,
  createRinggWidgetController,
} from "@ringg/react-native";
import { createStaticUrlResolver } from "@ringg/core";

const URLS = {
  dev: { backendUrl: "https://calling-dev.ringg.ai/ca/api/v0", livekitUrl: "wss://ringg-ai-dev-92tubwpz.livekit.cloud" },
  stage: { backendUrl: "https://stage-api.ringg.ai/ca/api/v0", livekitUrl: "wss://mercury.webrtc-stage.ringg.ai" },
  prod: { backendUrl: "https://prod-api.ringg.ai/ca/api/v0", livekitUrl: "wss://mercury.webrtc.ringg.ai" },
};

export const RinggSupport = () => {
  const { controller, livekit } = useMemo(() => {
    const livekit = createLiveKitTransport();
    const controller = createRinggWidgetController(
      {
        agentId: "<your-agent-id>",
        authorization: "Bearer <your-token>",
        title: "Support",
        description: "How can we help?",
        defaultTab: "text", // or "audio"
      },
      {
        transport: livekit.transport,
        urlResolver: createStaticUrlResolver(URLS),
        micPermission: createNativeMicPermission(),
      },
    );
    return { controller, livekit };
  }, []);

  // Releases the microphone and the audio session — do not skip this.
  useEffect(() => () => {
    controller.destroy();
    livekit.dispose();
  }, [controller, livekit]);

  return <RinggWidget controller={controller} room={livekit.room} />;
};
```

Passing `room` is optional; it only enables the in-call audio visualizer.

### 3 · Mount it over your app

`RinggWidget` renders its own floating trigger and panel over whatever is
behind it, so make it the **last child** of your root view:

```tsx
<View style={{ flex: 1 }}>
  <YourApp />
  <RinggSupport />
</View>
```

Tap the trigger → the chat/voice panel opens. That is the whole integration.

## Configuration

`RinggWidgetConfig` — only `agentId` is required:

| Field | Type | Purpose |
|---|---|---|
| `agentId` | `string` | your Ringg agent (**required**) |
| `authorization` | `string` | bearer token for your account |
| `title` / `description` | `string` | panel header text |
| `defaultTab` | `"audio" \| "text"` | which mode the panel opens in |
| `hideTabSelector` | `boolean` | pin the widget to one mode |
| `defaultExpanded` | `boolean` | open the panel on mount (no trigger) |
| `bypassStartScreen` | `boolean` | trigger tap starts the call directly |
| `bypassFeedbackScreen` | `boolean` | skip the post-call rating screen |
| `clientOrigin` | `string` | **required for real calls** — see below |
| `variables` | `Record<string, …>` | values for `{{placeholders}}` in agent prompts |
| `theme` | `WidgetTheme` | colours, radii, button style (gradients supported) |
| `logoUrl` / `logoStyles` | `string` / `PortableStyles` | branding in the header |
| `buttons` | `ButtonsConfig` | per-button copy, icons and styles |
| `legalDisclaimer` | `{ text, links }` | copy under the start buttons |
| `feedbackScreen` | `FeedbackScreenConfig` | rating screen copy and styling |
| `voiceCall` | `{ showAnimation, showTranscript }` | voice view options |
| `enabledSlashCommands` | `SlashCommand[]` | commands offered in the composer |
| `eventLogs` | `{ enabled, showIds }` | inline pills for agent-triggered actions |

`widgetPosition` and `innerWindowProps` are web-only and ignored here — the
panel sizes itself to the device.

## Caller identity — required for real calls

The backend allow-lists an agent's callers by the `Origin` header. A browser
sends it automatically; a native app sends nothing, so the webcall request is
rejected **before authentication is even considered**:

| response | meaning |
|---|---|
| `400 Origin header is required` | no `clientOrigin` was set |
| `403 Client '…' is not allowed` | it was set, but is not on the agent's list |
| `401 Invalid credentials` | the token is wrong for that environment |

Pass your app's identity, and add the same string to the agent's allowed
clients in the dashboard:

```ts
import { Platform } from "react-native";
import { appOrigin, createRinggWidgetController } from "@ringg/react-native";

const BUNDLE_ID = Platform.OS === "android" ? "com.acme.app" : "com.acme.App";

createRinggWidgetController(
  { agentId: "…", authorization: "Bearer …", clientOrigin: appOrigin(BUNDLE_ID) },
  ports,
);
```

`appOrigin` returns `<platform>://<bundleId>` — e.g. `android://com.acme.app`.
The bundle id is a parameter because React Native cannot read it without a
native module, and this package will not add one for a single string. Your app
already declares it, so a constant (or a read of your own app config) is
enough. `expo-application` reports the same value if you would rather ask the
OS — but it is native code, so adding it requires rebuilding the app, not just
restarting the bundler.

## Host events

The controller emits the same events as the web widget. On RN they are
in-memory rather than DOM events:

```ts
const unsubscribe = controller.eventBus.on("ringg:conversation_status", ({ status, mode, callId }) => {
  analytics.track(`call_${status}`, { mode, callId });
});
```

Events: `ringg:widget_status`, `ringg:conversation_status`,
`ringg:feedback_status`, `ringg:calendar_booking`,
`ringg:component_acknowledgement`.

## Agent-triggered app actions

Agents can fire host actions (`execute_dom_action` on the wire). On web these
become `CustomEvent`s; RN has no ambient event bus, so you supply the handler
and receive the same payload:

```ts
import { createHostActionDispatcher } from "@ringg/react-native";

const ports = {
  // ...
  onDomAction: createHostActionDispatcher(({ name, payload }) => {
    if (name === "open_checkout") navigation.navigate("Checkout", payload);
  }),
};
```

## Notification sound

React Native has no audio playback of its own, and every option is a native
module — so the widget ships **silent** rather than forcing a dependency on
every integrator. Wire whichever player your app already has:

```ts
import { createAudioPlayer } from "expo-audio";
import { createNotificationPlayer } from "@ringg/react-native";
import { DEFAULT_CONFIG } from "@ringg/core";

const notification = createNotificationPlayer(DEFAULT_CONFIG.notificationTuneUrl, (url) => createAudioPlayer(url).play());
```

Pass it as `ports.notification`.

## Testing hooks

Every meaningful node carries a `testID` mirroring the web widget's
`data-ringg` name, prefixed with `ringg-` — `ringg-trigger-button`,
`ringg-widget-root`, `ringg-header-title`, `ringg-message-input`,
`ringg-end-call-confirm`, and so on. These are a contract: they will not be
renamed without a major version.

Two web names have no RN counterpart, because they mark screen-reader-only
nodes and RN has no visually-hidden text: `ringg-header-status` and
`ringg-connecting-label`'s `sr-only` sibling. That copy lives in
`accessibilityLabel` on the surrounding node instead.

## Example app

A runnable Expo harness lives in [`example/`](./example) — it is the reference
integration and runs fully offline (mock transport + in-process backend) when
no credentials are configured.

## Known issues

- **Breaking changes between releases.** APIs may shift while pre-1.0.
- **No frosted-glass blur.** The web widget's header and composer blur what
  scrolls under them; RN has no blur primitive without a native dependency, so
  those surfaces are near-opaque instead.
- **No gradient-filled text.** The typing indicator's shimmer sweeps a gradient
  through the glyphs on web. RN cannot fill text with a gradient without a
  masking dependency, so the label takes the colour that sweep averages to and
  the motion moves into the animated ellipsis beside it.
- **Markdown is a subset.** Agent replies render bold, italic, inline code,
  links, lists, headings, code blocks, blockquotes and rules. Tables are not
  supported and render as plain text.
- **Voice on emulators is unreliable.** Android emulator networking often
  cannot establish the media connection, and iOS simulators expose no
  microphone or playout device. Test voice on a real device.
- **The policy-finder pack is web-only.** That integrator-specific flow is not
  part of this package.
