Skip to main content

React Native

@chativa/rn-webview embeds the existing web widget (@chativa/ui + @chativa/genui) inside a react-native-webview, bridged to native props and callbacks. Because the widget's JavaScript genuinely runs inside the WebView's browser engine, you get full feature parity with the web build — every message type, every GenUI component, and every existing @chativa/connector-* package works completely unmodified.

A from-scratch native RN UI is tracked separately per platform (#29 iOS, #30 Android) as a conditional follow-up — only if the WebView approach doesn't meet the UX bar in practice.

Install

pnpm add @chativa/rn-webview react-native-webview

Peer dependencies: react >= 18.2, react-native >= 0.74, react-native-webview >= 13. The widget scripts themselves are loaded from a CDN inside the WebView (jsdelivr by default) — see CDN and versions. The loaded @chativa/ui build must be >= 0.10: the bridge binds window.Chativa.EventBus / window.Chativa.chatStore (the singletons inside the ui bundle), and older builds don't expose them — the bridge then reports a loud onError instead of silently observing a disconnected copy of core.

Quick start

import { ChativaWebView } from "@chativa/rn-webview";

<ChativaWebView
connector={{ type: "dummy", options: { replyDelay: 500 } }}
theme={{ colors: { primary: "#4f46e5" } }}
onMessage={(m) => console.log(m)}
style={{ flex: 1 }}
/>;

The widget always renders in inline window mode and opens immediately — there is no launcher button in this embedding; the RN screen presenting the WebView is the launcher.

Connector spec

A live IConnector instance can't cross the native/web bridge, so the connector prop describes which connector to construct inside the WebView (ChativaConnectorSpec). options must be JSON-serializable.

Built-in types (loaded from each connector package's CDN build): dummy, websocket, signalr, directline, http, sse, mekik.

A private connector works via type: "custom":

<ChativaWebView
connector={{
type: "custom",
scriptUrl: "https://cdn.example.com/my-connector.global.js",
globalName: "MyConnector", // window.<globalName> the script assigns itself to
className: "MyConnector", // exported class to instantiate from that global
options: { url: "wss://bot.example.com" },
}}
/>

Mekik and server-defined GenUI

connector: { type: "mekik", options } connects the WebView-hosted widget straight to a mekik server. Server-defined GenUI components render inside the WebView exactly as on the web; React Native observes the flow through the GenUI callbacks:

<ChativaWebView
connector={{
type: "mekik",
options: {
url: "ws://192.168.1.10:8790/chat", // a device can't reach your machine's `localhost`
resumeConversation: true,
auth: { kind: "token", token: "my-api-key" },
},
}}
onGenUIComponentsRegistered={({ components }) => console.log("catalog:", components)}
onGenUIStreamStarted={({ streamId }) => console.log("stream started", streamId)}
onGenUIStreamCompleted={({ streamId }) => console.log("stream done", streamId)}
onAuthError={({ code, message }) => console.warn("auth rejected:", code, message)}
style={{ flex: 1 }}
/>
  • onGenUIComponentsRegistered delivers summaries only ({ name, version?, tag? }) — the full template stays inside the WebView where it renders.
  • Component events (component-event / mekik-event / data-event buttons in a rendered widget) round-trip to the server entirely inside the WebView; RN doesn't need to participate.

Auth is described, not passed. MekikConnectorOptions.auth normally takes a live TokenAuth / CookieAuth instance, but a class instance can't cross the JSON bridge. The spec's auth field (MekikAuthSpec) is a plain object rebuilt into the real adapter inside the WebView:

SpecRebuilt asNotes
{ kind: "token", token, transport?, queryParam?, maxRetries? }new TokenAuth({...})Only a string token — a token-minting function can't be serialized.
{ kind: "cookie" }new CookieAuth()The refresh callback is likewise unsupported over the bridge.

onAuthError can't be passed inside options either; the bootstrap wires it and surfaces rejections as the onAuthError prop. Apps that need function credentials should host the widget with @chativa/react or supply a custom connector script that builds its own provider.

Props

PropDescription
connectorChativaConnectorSpec — required, see above.
themeDeepPartial<ThemeConfig> applied on load.
locale, i18nSame shape as ChativaSettings (flat i18n overrides only).
cdnBaseUrlBase URL scripts are resolved against. Default: jsdelivr @latest per package.
versionsPin exact versions per package ({ ui?, dummy?, websocket?, ... }) instead of @latest.
styleStyleProp<ViewStyle> for the underlying WebView.

Callback props (each is the bridge event it maps to — see the bridge reference): onReady, onMessage, onMessageSent, onConnect, onDisconnect, onSurveySubmit, onWidgetOpen, onWidgetClose, onGenUIComponentsRegistered, onGenUIStreamStarted, onGenUIStreamCompleted, onToolCallUpdated, onAuthError, onError.

Live commands after mount

Changing connector / theme props does not re-render the HTML — that would fully reload the WebView and reconnect. Live updates go through the bridge instead, via sendToChativaWebView(ref, msg):

import { useRef } from "react";
import type WebView from "react-native-webview";
import { ChativaWebView, sendToChativaWebView } from "@chativa/rn-webview";

const webViewRef = useRef<WebView>(null);

sendToChativaWebView(webViewRef, { type: "set_theme", payload: { colors: { primary: "#000" } } });
sendToChativaWebView(webViewRef, { type: "send_message", payload: { text: "Hello" } });
sendToChativaWebView(webViewRef, { type: "open_widget" });
sendToChativaWebView(webViewRef, { type: "close_widget" });

Bridge message reference

Outbound (WebView → RN), delivered as the matching callback prop:

messagepayloadprop
readyonReady
message_receivedIncomingMessageonMessage
message_sentOutgoingMessageonMessageSent
connector_status_changed{ status }onConnect / onDisconnect
survey_submittedSurveyPayloadonSurveySubmit
widget_opened / widget_closedonWidgetOpen / onWidgetClose
genui_components_registered{ components: [{ name, version?, tag? }] }onGenUIComponentsRegistered
genui_stream_started / genui_stream_completed{ streamId }onGenUIStreamStarted / onGenUIStreamCompleted
tool_call_updatedToolCallonToolCallUpdated
auth_error{ code, message }onAuthError
error{ message }onError

Inbound (RN → WebView), via sendToChativaWebView(ref, msg):

messagepayloadeffect
set_themeDeepPartial<ThemeConfig>Live theme update.
send_message{ text, markdown? }Sends a user message (same path as typing it).
open_widget / close_widgetchatStore.open() / .close().

CDN and versions

The bootstrap page (built by the exported buildBootstrapHtml()) loads @chativa/ui and the requested connector from each package's CDN/global (IIFE) build — https://cdn.jsdelivr.net/npm/<pkg>@latest/dist/... by default. Override cdnBaseUrl and/or versions to pin exact versions or point at bundled local assets for offline use.

Known gaps

  • File/image upload isn't bridged yetsendFile in the embedded widget only works with whatever the WebView's own file input gives it.
  • i18n per-language overrides — only the flat format (applied to every language) is wired through, matching @chativa/react's scope decision.
  • No offline/bundled-asset story yet — the device needs network access to jsdelivr (or an override URL).
  • No RN-originated GenUI component events — a rendered widget's buttons round-trip inside the WebView, but native code can't synthesize a component event.

Example app

examples/rn-webview-expo is a runnable Expo app with a dummy/mekik connector toggle, GenUI event logging and bridge command buttons.