Add your bot in any React Native or Expo app using react-native-webview.
Why a WebView? Botpress Cloud Webchat is a browser client (window.botpress). React Native does not run that API natively, so the supported pattern is to run the official Webchat inside a WebView and communicate with postMessage and injectJavaScript.The @botpress/webchat package targets web React. For native apps, use the WebView approach below.
Create a src folder at your project root if you do not already have one. A typical layout:
Path
Purpose
src/botpress/getBotpressWebchat.js
Builds { html, baseUrl } for the WebView.
src/botpress/BpWidget.js
WebView component and event bridge (onMessage); blocks marketing site navigation on close.
src/botpress/BpWidget.d.ts
(Optional, TypeScript only) Types for BpWidget.
src/config/botpressConfig.js
Webchat URLs and optional botId. Step 4 shows what goes inside export const botpressConfig = { ... }.
You can instead keep files flat under src/ and use relative imports. The examples below assume src/botpress/ and src/config/.Path alias (optional, Expo / TypeScript): In tsconfig.json, merge into compilerOptions:
4. Save your Studio script URLs in one config file
Create src/config/botpressConfig.js and export a single botpressConfig object. Paste the script URLs from Studio → Webchat → Embed (the same values from your embed snippet). The HTML helper and the widget only read botConfig: you pass it in, and you do not duplicate URLs inside getBotpressWebchat.js or BpWidget.js.
Sensitive values:botId and embed URLs identify your bot. For a public repository, do not commit real values, use placeholders in git or keep this file out of version control.
Add src/botpress/getBotpressWebchat.js. It builds a tiny HTML page (inject script, embed script, full height container) and returns { html, baseUrl } so you can pass it straight into <WebView source={{ baseUrl, html }} />.
getBotpressWebchat.js
/** * Builds the HTML page loaded by the WebView for Botpress Cloud webchat v3. * @param {Record<string, unknown>} botConfig Pass `botpressConfig` from src/config/botpressConfig.js * @returns {{ html: string, baseUrl: string }} */const getBotpressWebchat = (botConfig) => { // Official inject script (Botpress loader); falls back to a known default if omitted. const injectUrl = botConfig.injectScriptUrl || "https://cdn.botpress.cloud/webchat/v3.6/inject.js"; // Studio-generated bundle URL (required). This wires your bot into the page. const embedUrl = botConfig.embedScriptUrl; if (!embedUrl || typeof embedUrl !== "string") { throw new Error( "botConfig.embedScriptUrl is required (paste the second script URL from Studio embed)", ); } // Origin passed to WebView as `baseUrl` so relative URLs in the HTML resolve correctly. const baseUrl = botConfig.baseUrl || "https://cdn.botpress.cloud/"; // Minimal HTML shell: viewport, full-height container, then inject + embed scripts (matches Studio order). const html = `<!DOCTYPE html><html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <style> html, body { margin: 0; height: 100%; } body { display: flex; flex-direction: column; height: 100vh; } #bp-web-widget-container { height: 100%; width: 100%; flex: 1; } </style> <title>Chatbot</title> </head> <body> <script src="${injectUrl}"></script> <script src="${embedUrl}" defer></script> </body></html>`; // WebView expects this pair: `source={{ baseUrl, html }}`. return { baseUrl, html };};module.exports = getBotpressWebchat;
6. Show Webchat in a WebView and forward events to React Native
Create src/botpress/BpWidget.js. It renders a WebView that loads your HTML, then relays what happens inside the page to your React Native layer:
Puts the page from getBotpressWebchat(botConfig) into the WebView.
Injects a script that subscribes to window.botpress and sends each event to React Native with postMessage (you read them in onMessage as JSON strings).
Intercepts navigation to botpress.com or *.botpress.com when the user closes the widget and runs window.botpress.close() instead to close the webchat interface.
BpWidget.js
/** * WebView wrapper for Botpress webchat v3: window.botpress API, events to React Native via postMessage. */import { WebView } from "react-native-webview";import getBotpressWebchat from "./getBotpressWebchat";import React, { useCallback, useRef } from "react";const broadcastToReactNative = `(function () { // Sends one event payload to React Native as a JSON string (read in onMessage). function post(ev, data) { try { window.ReactNativeWebView.postMessage(JSON.stringify({ event: ev, data: data })); } catch (e) {} } // Subscribes to window.botpress once it exists and mirrors events into React Native. function wire() { if (!window.botpress || typeof window.botpress.on !== "function") return false; window.botpress.on("webchat:initialized", function () { try { window.botpress.open(); } catch (e) {} }); [ "message", "webchat:initialized", "webchat:ready", "webchat:opened", "webchat:closed", "customEvent", "error", "conversation", ].forEach(function (ev) { window.botpress.on(ev, function (data) { post(ev, data); }); }); // Fallback open in case the widget did not auto-open after init. setTimeout(function () { try { window.botpress.open(); } catch (e) {} }, 400); return true; } // Poll until botpress is ready (or give up after ~12s) so inject order does not race. var n = 0; var id = setInterval(function () { if (wire() || ++n > 240) clearInterval(id); }, 50);})();true;`;const closeChatJs = `try { // Programmatic close when we block a navigation attempt (through onShouldStartLoadWithRequest). if (window.botpress && typeof window.botpress.close === "function") { window.botpress.close(); }} catch (e) {}true;`;// Returns true when a navigation URL is the public Botpress marketing site (close button behavior).function isBotpressMarketingSiteUrl(url) { if (!url || url.startsWith("about:") || url.startsWith("blob:") || url.startsWith("data:")) { return false; } try { const h = new URL(url).hostname; return h === "botpress.com" || h.endsWith(".botpress.com"); } catch { return false; }}export default function BpWidget(props) { const { botConfig, onMessage } = props; const webref = useRef(null); // Build the in-memory HTML page and origin for this WebView instance. const { html, baseUrl } = getBotpressWebchat(botConfig); // Intercept navigations: block opening botpress.com inside the frame and close chat instead. const onShouldStartLoadWithRequest = useCallback((request) => { const { url } = request; if (isBotpressMarketingSiteUrl(url)) { webref.current?.injectJavaScript(closeChatJs); return false; } return true; }, []); // Webchat needs JavaScript and DOM storage; originWhitelist allows loading scripts from Botpress CDNs. return ( <WebView ref={webref} style={{ flex: 1 }} source={{ baseUrl, html, }} onMessage={onMessage} injectedJavaScript={broadcastToReactNative} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} setSupportMultipleWindows={false} javaScriptEnabled domStorageEnabled originWhitelist={["*"]} /> );}
onMessage format:event.nativeEvent.data is a string. Parse with JSON.parse. Shape: { event: string, data: unknown }.
Import BpWidget and botpressConfig, then render it full screen or inside any View that uses flex: 1. Adjust import paths to your real paths (for example Expo Router app/index.tsx, a root App.js, or src/screens/…).
ChatScreen.tsx
import { StyleSheet, View } from "react-native";import { SafeAreaView } from "react-native-safe-area-context";import BpWidget from "@/botpress/BpWidget";import { botpressConfig } from "@/config/botpressConfig";/** Screen that fills the safe area with the Botpress WebView-powered widget. */export default function ChatScreen() { return ( <SafeAreaView style={styles.safeArea}> <View style={styles.main}> <BpWidget botConfig={botpressConfig} onMessage={(e) => { // Bridge: WebView posts JSON strings; parse to `{ event, data }` for logging or app logic. const raw = e.nativeEvent?.data; if (!raw) return; try { const msg = JSON.parse(raw); console.log(msg.event, msg.data); } catch { /* non-JSON */ } }} /> </View> </SafeAreaView> );}const styles = StyleSheet.create({ safeArea: { flex: 1 }, main: { flex: 1 },});
embedScriptUrl in botpressConfig.js must be the files.bpcontent.cloud/... URL from Studio.
Error: embedScriptUrl is required
Set embedScriptUrl in botpressConfig.
Close (X) opens the Botpress.com website
Keep onShouldStartLoadWithRequest and closeChatJs in BpWidget.js as shown in that section.
Fonts look wrong
Tune appearance in Botpress Studio; avoid forcing global font-family in the HTML shell.
Android keyboard covers the composer
In app.json (Expo), under expo.android, set "softwareKeyboardLayoutMode": "resize". Rebuild the native Android app after changing this (not only a Metro refresh).