Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @chativa/rn-webview
run: pnpm publish --access public --no-git-checks
working-directory: packages/rn-webview
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
lib
*.local

# Docusaurus
Expand Down
41 changes: 41 additions & 0 deletions examples/rn-webview-expo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android
81 changes: 81 additions & 0 deletions examples/rn-webview-expo/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useRef } from "react";
import { Button, SafeAreaView, StyleSheet, Text, View } from "react-native";
import { StatusBar } from "expo-status-bar";
import { ChativaWebView, sendToChativaWebView } from "@chativa/rn-webview";
import type WebView from "react-native-webview";

export default function App() {
const webViewRef = useRef<WebView>(null);

return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>@chativa/rn-webview example</Text>
<Text style={styles.subtitle}>
ChativaWebView embeds the same web widget used by @chativa/react —
the connector runs entirely inside the WebView's own browser
engine, so DummyConnector (and any @chativa/connector-* package)
works completely unmodified.
</Text>
<View style={styles.buttonRow}>
<Button
title="Set dark accent theme"
onPress={() =>
// Live theme update after mount goes through the bridge, not a
// prop change — changing connector/theme props would fully
// reload the WebView and reconnect. See the package README.
sendToChativaWebView(webViewRef, {
type: "set_theme",
payload: { colors: { primary: "#111827" } },
})
}
/>
</View>
</View>

<ChativaWebView
ref={webViewRef}
style={styles.webview}
connector={{ type: "dummy", options: { replyDelay: 500, connectDelay: 0 } }}
theme={{ colors: { primary: "#7c3aed", secondary: "#4f46e5" } }}
onReady={() => console.log("[chativa] bridge ready")}
onMessage={(message) => console.log("[chativa] message received:", message)}
onMessageSent={(message) => console.log("[chativa] message sent:", message)}
onConnect={() => console.log("[chativa] connector connected")}
onError={(message) => console.warn("[chativa] bridge error:", message)}
/>

<StatusBar style="auto" />
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f8fafc",
},
header: {
paddingHorizontal: 20,
paddingTop: 12,
paddingBottom: 16,
},
title: {
fontSize: 20,
fontWeight: "700",
color: "#0f172a",
},
subtitle: {
marginTop: 6,
fontSize: 13,
lineHeight: 18,
color: "#475569",
},
buttonRow: {
marginTop: 12,
alignItems: "flex-start",
},
webview: {
flex: 1,
},
});
43 changes: 43 additions & 0 deletions examples/rn-webview-expo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# @chativa/rn-webview example (Expo)

Living usage doc for [`@chativa/rn-webview`](../../packages/rn-webview) —
`<ChativaWebView>` wired to `@chativa/connector-dummy`, plus a live
theme-update button using `sendToChativaWebView`.

## Running it

From the repo root (this is a pnpm workspace member, not a standalone app):

```sh
pnpm install
pnpm --filter chativa-example-rn-webview-expo start
```

Then scan the QR code with **Expo Go** (iOS/Android) — no custom dev client
or native build needed, since `react-native-webview` is one of the modules
Expo Go ships with.

This example was scaffolded with `create-expo-app` and depends on
`@chativa/rn-webview`/`@chativa/core` via `workspace:*`. Expo/Metro (SDK 52+)
auto-detects the pnpm workspace root from `pnpm-workspace.yaml` — no manual
`metro.config.js` needed.

## What it shows

- `connector={{ type: "dummy", options: {...} }}` — the connector runs
*inside* the WebView's own browser engine, not in RN's JS runtime, so this
is the exact same `DummyConnector` class `@chativa/connector-dummy` ships
for the web, completely unmodified.
- `onMessage` / `onMessageSent` / `onConnect` / `onReady` / `onError` — the
bridge surfaces the same event shape `@chativa/react`'s `<ChatIva>` does.
- `sendToChativaWebView(ref, { type: "set_theme", payload })` — pushing a
live update into an already-mounted WebView, rather than relying on a prop
change (which would reload the page and reconnect).

## Not verified by an automated check

Unlike `examples/react-vite` (typechecked *and* exercised in a real browser
via chrome-devtools during development), this example is typechecked but was
**not** run in an actual Expo Go client/simulator — that requires a device or
emulator this environment doesn't have. If something doesn't work when you
actually run it, that's the gap to check first.
25 changes: 25 additions & 0 deletions examples/rn-webview-expo/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"expo": {
"name": "rn-webview-expo",
"slug": "rn-webview-expo",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/android-icon-foreground.png",
"backgroundImage": "./assets/android-icon-background.png",
"monochromeImage": "./assets/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/rn-webview-expo/assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/rn-webview-expo/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/rn-webview-expo/assets/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions examples/rn-webview-expo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';

import App from './App';

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
27 changes: 27 additions & 0 deletions examples/rn-webview-expo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "chativa-example-rn-webview-expo",
"private": true,
"version": "1.0.0",
"description": "Living usage doc for @chativa/rn-webview — <ChativaWebView> wired to @chativa/connector-dummy in an Expo app.",
"main": "index.ts",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@chativa/core": "workspace:*",
"@chativa/rn-webview": "workspace:*",
"expo": "~57.0.8",
"expo-status-bar": "~57.0.1",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native-webview": "^14.0.1"
},
"devDependencies": {
"@types/react": "~19.2.2",
"typescript": "~6.0.3"
}
}
14 changes: 14 additions & 0 deletions examples/rn-webview-expo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
// Points at source rather than the built `lib/` output — matches how
// Metro actually resolves this package at runtime too (via its
// package.json "react-native" field, which also points at src/index.ts),
// so this isn't just a typecheck convenience.
"paths": {
"@chativa/rn-webview": ["../../packages/rn-webview/src/index.ts"],
"@chativa/core": ["../../packages/core/src/index.ts"]
}
}
}
119 changes: 119 additions & 0 deletions packages/rn-webview/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# @chativa/rn-webview

Embeds the existing Chativa web chat widget inside a `react-native-webview`,
bridged to native props/callbacks — instead of re-implementing the UI as
native RN views. Tracks [AimTune/chativa#6](https://github.com/AimTune/chativa/issues/6).

## Why WebView instead of native components

The package started out scoped as a from-scratch native re-implementation
(RN `View`/`FlatList`/`Pressable` + FlashList). That was reconsidered: a
WebView wrapper around the *already-shipping* `@chativa/ui` +
`@chativa/genui` widget gets full feature parity with the web build for
free — every message type, every GenUI component, every connector — with no
UI code to port or keep in sync. The tradeoff is WebView UX limits
(keyboard resize timing, scroll/momentum feel, animation smoothness), which
are the reason a **true native UI is tracked as a separate, conditional
follow-up per platform**:

- iOS: [AimTune/chativa#29](https://github.com/AimTune/chativa/issues/29)
- Android: [AimTune/chativa#30](https://github.com/AimTune/chativa/issues/30)

Both are explicitly scoped as "only build this if the WebView approach turns
out to be insufficient in a real app" — not committed work.

## How it works

`<ChativaWebView>` renders a `react-native-webview` pointed at a
self-contained HTML document built by `buildBootstrapHtml()`. That document:

1. Loads `@chativa/core`, `@chativa/ui` (which itself pulls in
`@chativa/genui`), and whichever connector script the `connector` prop
asks for — each from that package's own CDN/global (IIFE) build, via
jsdelivr by default (`https://cdn.jsdelivr.net/npm/<pkg>@latest/dist/...`).
Override `cdnBaseUrl`/`versions` to pin exact versions or point at bundled
local assets for offline use.
2. Constructs the connector (`new window.<Global>.<ClassName>(options)`) and
assigns `window.chativaSettings` — the *same* global-config convention
`@chativa/core`'s `applyGlobalSettings()` already reads on the web, just
built dynamically instead of hand-written.
3. Forces `windowMode: "inline"` and calls `chatStore.getState().open()` so
the widget fills the WebView immediately — there's no separate launcher
button in this embedding (the RN screen presenting the WebView *is* the
launcher).
4. Mounts `<chat-iva>` and bridges `@chativa/core`'s `EventBus` to
`ReactNativeWebView.postMessage`, which `<ChativaWebView>` turns back into
the same event props `@chativa/react`'s `<ChatIva>` exposes: `onMessage`,
`onMessageSent`, `onConnect`, `onDisconnect`, `onSurveySubmit`,
`onWidgetOpen`, `onWidgetClose`.

Because the widget's JS (core + connector + UI) genuinely runs inside the
WebView's browser engine, **every existing `@chativa/connector-*` package
works completely unmodified** — `connector-websocket`, `-signalr`,
`-directline`, `-sse`, `-http` all already have CDN builds (see each
package's `jsdelivr` field / `vite.config.cdn.ts`). A connector without one
(e.g. `@chativa/connector-mekik`, or a private connector) still works via
`{ type: "custom", scriptUrl, globalName, className, options }`.

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

<ChativaWebView
connector={{ type: "dummy", options: { replyDelay: 500 } }}
onMessage={(m) => console.log(m)}
style={{ flex: 1 }}
/>;
```

Live theme updates after mount go through the bridge rather than a prop
change (changing `connector`/`theme` props doesn't re-render the HTML — that
would fully reload the WebView and reconnect):

```tsx
import { sendToChativaWebView } from "@chativa/rn-webview";

sendToChativaWebView(webViewRef, { type: "set_theme", payload: { colors: { primary: "#000" } } });
```

## Building

`pnpm build` runs `react-native-builder-bob` (`module` + `commonjs` +
`typescript` targets). Unlike `@chativa/ui`/`@chativa/genui` — which alias
`@chativa/core` to its *source* in `tsconfig.json` so their vite +
`vite-plugin-dts` build can roll everything into one bundled `.d.ts` — this
package resolves `@chativa/core` through its normal **built** `dist/index.d.ts`
(plain `node_modules` resolution). Bob's `typescript` target is a bare `tsc`
declaration emit with no rollup step, and aliasing to source there made `tsc`
mirror `@chativa/core`'s entire source tree into this package's own
`lib/typescript` output instead of a clean `src/index.d.ts`. Practical effect:
**run `pnpm --filter @chativa/core build` at least once before this
package's `typecheck`/`build` will see local `@chativa/core` changes** — in
the release pipeline this is a non-issue since `pnpm -r build` already
builds packages in dependency order.

## Known gaps

- **File/image upload isn't bridged yet.** A plain HTML `<input type="file">`
inside a WebView is a poor mobile experience; the intent is for the web
side to ask RN to open `react-native-image-picker` (or the Expo
equivalent) and inject the result back over the bridge, but that round
trip isn't implemented — `sendFile` in the embedded widget currently 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 `buildBootstrapHtml`, matching
`@chativa/react`'s `ChativaProvider` scope decision, not the full
per-language `ChativaSettings.i18n` shape.
- **No offline/bundled-asset story yet** — `cdnBaseUrl` accepts a local
`file://` base in principle, but nothing here packages `dist/*.global.js`
files as RN assets; today this requires the device to have network access
to jsdelivr (or an override URL).
- No tests yet.

## `DOM` leaks in `@chativa/core`

No longer a blocker for *this* package specifically — `@chativa/core` runs
*inside* the WebView's real browser engine here, where `window`/`File`/
`HTMLElement` genuinely exist. It would still matter for the native-fallback
issues (#29/#30) if those end up running any core logic directly in RN's own
JS runtime (Hermes) rather than only using `@chativa/core` for types on that
side too.
3 changes: 3 additions & 0 deletions packages/rn-webview/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: ["module:@react-native/babel-preset"],
};
Loading
Loading