Native React Native chat UI for Chatty — zero WebView, zero compromise.
Drop a fully native, on-brand support chat into any React Native or Expo app in minutes. Talks
directly to the same /api/widget/* backend as the Chatty web widget, and renders every bubble,
avatar, and composer with real View/Text/FlatList components — no WebView, no JS bridge.
Install · Quick start · Design gallery · API reference · Example app
| No WebView, anywhere | Every bubble, avatar, and the composer are real RN components — no iframe, no JS bridge, no WebView memory/perf overhead. |
| Matches your dashboard automatically | Fetches the bot's theme and renders with the exact colors, corner radii, and launcher shape chosen in the dashboard — no manual styling. |
| Three integration shapes | A floating ChattyLauncher, an embedded ChattyChatView, or the headless useChattyChat hook. |
| A real composer, not a stub | Full-Unicode emoji picker (search + categories, ~1,850 emoji) and an animated attach menu (camera, photo, documents, location), built in — each wires up to whatever picker/recorder/location API your app already uses. |
| Works with bare RN and Expo | No native linking required beyond @react-native-async-storage/async-storage, which most apps already have. |
npm install @personaliai/react-native @react-native-async-storage/async-storagereact and react-native are peer dependencies — install versions matching your app (react
18+, react-native 0.72+). Voice calls (ChattyVoiceCallView) are the one feature that needs
native linking — see Voice-call button — everything else needs nothing beyond
@react-native-async-storage/async-storage.
Find your bot ID in the Chatty dashboard under Embed & Integrate → React Native SDK.
Floating launcher (recommended) — a bubble that expands into a full-screen modal, the native equivalent of the web widget's launcher button:
import { ChattyLauncher } from "@personaliai/react-native";
export default function App() {
return (
<>
{/* ...your app... */}
<ChattyLauncher botId="YOUR_BOT_ID" position="right" />
</>
);
}Script Method (WebView Embed — 100% web widget parity) — loads the exact production widget served at /embed/{botId} with pixel-for-pixel visual and feature parity, including client-side voice messages, file attachments, AI search, and CSAT:
import { ChattyEmbedView } from "@personaliai/react-native";
function SupportScreen() {
return (
<ChattyEmbedView
botId="YOUR_BOT_ID"
onReady={() => console.log("Chat ready")}
onMessage={() => console.log("New message")}
onClose={() => navigation.goBack()}
onRequestNotificationPermission={(botName) => {
// Request POST_NOTIFICATIONS contextually (Android 13+ / iOS)
}}
onMicPermissionNeeded={() => {
// Request RECORD_AUDIO permission contextually when user taps the mic
}}
onLocationPermissionNeeded={() => {
// Request location permission contextually when user selects "Location"
}}
/>
);
}Native Components Chat — renders with 100% native React Native components (FlatList, View, Text, ChattyMarkdown):
import { ChattyChatView } from "@personaliai/react-native";
function SupportScreen() {
return <ChattyChatView botId="YOUR_BOT_ID" />;
}Headless — build your own UI
import { useChattyChat } from "@personaliai/react-native";
function CustomChat() {
const { messages, sendText, sending, theme } = useChattyChat({ botId: "YOUR_BOT_ID" });
// render messages and call sendText(text) yourself
}The SDK ships all 10 Chatty widget designs as color/radius tokens, ported 1:1 from the web
widget's globals.css, so a native screen looks like whatever design is chosen in the dashboard
rather than one generic look. No configuration required — ChattyChatView and ChattyLauncher
fetch the bot's theme and resolve the matching token set automatically, including legacy
widget_style IDs from older presets.
| Design | Accent |
|---|---|
minimal |
|
playful |
|
corporate |
|
dark-sleek |
|
gradient-glow |
|
glassmorphism |
|
ecommerce |
|
healthcare-calm |
|
neubrutalism |
|
luxury-editorial |
Font pairing (each web design uses a distinct Google Font) is intentionally out of scope for this release; color, radius, and header/bubble treatment carry most of a design's identity.
<ChattyLauncher
botId={string}
baseUrl={string} // optional, defaults to the production API
host={string} // optional, see Notes
position={"left" | "right"} // optional, defaults to "right"
onReady={() => void}
onMessage={(message) => void}
onVoiceCallPress={() => void}
onNotificationBellPress={() => void}
enableNotificationBell={boolean} // optional, defaults to true — see Permissions
/>The button color follows the active design's accent automatically — same as web.
<ChattyChatView
botId={string}
baseUrl={string} // optional, defaults to the production API
host={string} // optional, see Notes
onReady={() => void}
onMessage={(message) => void}
onCameraPress={() => void} // optional, "Camera" tapped in the attach menu
onPhotoLibraryPress={() => void} // optional, "Photo Library" tapped in the attach menu
onDocumentPress={() => void} // optional, "Documents" tapped in the attach menu
onShareLocationPress={() => void} // optional, "Location" tapped in the attach menu
onAttachPress={() => void} // optional fallback if the four above aren't given
onMicPress={() => void} // optional — mic button only renders when this is set
onVoiceCallPress={() => void} // optional, header voice-call button (only shown when
// the bot's dashboard has voice enabled)
onNotificationBellPress={() => void} // optional, header notification-bell button — see Notes
onClose={() => void} // optional, renders a header close (✕) button.
// ChattyLauncher passes this for you.
enableNotificationBell={boolean} // optional, defaults to true — see Permissions
/>The official web widget hosted in a React Native WebView. Guaranteed pixel-for-pixel and feature-for-feature parity with the web widget, with automatic permission reflection, alert suppression, and OEM font scale isolation:
<ChattyEmbedView
botId={string}
baseUrl={string} // optional, defaults to https://chatty.personaliai.com
onReady={() => void} // optional, fired when web widget finishes loading
onMessage={() => void} // optional, fired on every assistant reply
onClose={() => void} // optional, fired when user closes the widget / CSAT
onRequestNotificationPermission={(botName) => void} // optional, fired on notification bell tap
onMicPermissionNeeded={() => void} // optional, fired on mic tap if RECORD_AUDIO not granted
onLocationPermissionNeeded={() => void} // optional, fired on location attach if not granted
/>Real CommonMark + GFM (tables, strikethrough, blockquotes, code blocks with copy) + LaTeX equation support ($inline$ and $$block$$), matching web widget's feature set. Used automatically by ChattyChatView and available standalone:
import { ChattyMarkdown } from "@personaliai/react-native";
<ChattyMarkdown
text="Solve $$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$ for $x$."
color="#111827"
fontSize={13}
/>Note
This SDK renders the composer's emoji picker and attach/mic UI (with layout animation), but it
deliberately doesn't bundle a camera, photo-library, document-picker, geolocation, or
audio-recording dependency itself — that would mean forcing every consumer (bare RN and Expo
alike) to install and link a native module they might not want. Instead, onCameraPress /
onPhotoLibraryPress / onDocumentPress / onShareLocationPress / onMicPress fire when
their button is tapped so you can wire up whichever picker/recorder/location API your app
already uses (expo-image-picker + expo-av + expo-document-picker + expo-location,
react-native-image-picker, etc.) and then call sendImage yourself — for location, matching
the web widget's behavior means setting the composer text to a Google Maps link rather than
sending a special message type. The header's clear-chat button (↺) is fully built in and needs
no wiring — it resets local messages and starts a fresh session.
const {
theme, ready, messages, sending, aiPaused, error,
sendText, sendImage, clearChat,
} = useChattyChat({ botId: string, baseUrl?: string, host?: string });Everything ChattyChatView uses internally — conversation state, polling, and the
send/sendImage/clearChat actions — with no UI attached, for apps that want to render their own
layout.
Permissions — what this SDK requests, and how to opt out
This SDK strictly adheres to the "Reflect, Never Request" contract:
| Feature | Android Permission | iOS Info.plist Key | Callback |
|---|---|---|---|
Voice Notes (getUserMedia) |
android.permission.RECORD_AUDIOandroid.permission.MODIFY_AUDIO_SETTINGS |
NSMicrophoneUsageDescription |
onMicPermissionNeeded |
Location Share (geolocation) |
android.permission.ACCESS_COARSE_LOCATIONandroid.permission.ACCESS_FINE_LOCATION |
NSLocationWhenInUseUsageDescription |
onLocationPermissionNeeded |
| Notifications | android.permission.POST_NOTIFICATIONS |
N/A (UserNotifications API) | onRequestNotificationPermission |
In ChattyEmbedView, the SDK checks permissions read-only and reflects the status to the web widget. If a user taps the mic or location attach without permissions, onMicPermissionNeeded or onLocationPermissionNeeded fires so you can present your own rationale and request the permission. Once granted, returning to the view or subsequent taps immediately succeed.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest><key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone to record voice messages for support.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to share with support.</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera to attach photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to attach photos.</string>Security — bot_id and domain restriction
bot_id is not a secret — it's extractable from any client, web or mobile. Domain restriction
(allowed_domains in the dashboard) is enforced by the backend as a rate-limit tier, not a
hard reject: verified web traffic gets 30 msgs/60s per bot+IP, everything else (including all
mobile SDK traffic — there's no way for a native app to obtain a "verified" token the way a
browser's Referer allows) gets throttled to 5 msgs/120s. The host prop this SDK sends is
advisory only and isn't used for access control. If your bot is mobile-primary, leave
allowed_domains empty to get the normal 30/60s tier instead.
Notification bell — what it does and doesn't do
Tapping it requests POST_NOTIFICATIONS on Android 13+ (PermissionsAndroid, built into RN
core — no extra dependency) and then calls onNotificationBellPress. There's no cross-platform
JS API for the permission ask on iOS — request it yourself (e.g. via expo-notifications)
before/inside the callback. Either way, that's as far as this SDK goes: actually delivering a
push when a reply arrives while the app is backgrounded needs FCM/APNs wired up at the app level
(register the device token, send it to your backend, store it against the session/user, call
FCM/APNs when a message lands for a session that isn't actively polling) — none of that exists
yet, it's backend work in chatty-backend.
Voice-call button
Only shown when the bot's dashboard has voice enabled, and fires onVoiceCallPress. This SDK now
ships a ready-to-render call screen, ChattyVoiceCallView — render it yourself from that callback
(it's opt-in, not wired in automatically, so apps that never use voice don't pay for the
dependency):
import { ChattyChatView, ChattyVoiceCallView, ChattyClient } from "@personaliai/react-native";
import { registerGlobals } from "@livekit/react-native"; // once, at app startup (e.g. index.js)
registerGlobals();
function MyChat() {
const [showCall, setShowCall] = useState(false);
const client = useMemo(() => new ChattyClient({ botId: "YOUR_BOT_ID" }), []);
if (showCall) {
return (
<ChattyVoiceCallView
client={client}
sessionId={sessionId} // the same session id ChattyChatView/useChattyChat is using
widgetStyle={theme?.widget_style}
onClose={() => setShowCall(false)}
/>
);
}
return <ChattyChatView botId="YOUR_BOT_ID" onVoiceCallPress={() => setShowCall(true)} />;
}Needs three additional peer dependencies only if you use this (npm i @livekit/react-native livekit-client @livekit/react-native-webrtc + the usual iOS pod install / Android autolinking) —
apps that don't render ChattyVoiceCallView don't need them. See LiveKit's own React Native setup
docs for platform permissions (NSMicrophoneUsageDescription on iOS, RECORD_AUDIO on Android).
- Lead capture and meeting booking happen conversationally (the assistant decides to ask/act) — there's no separate REST call to trigger them from the SDK.
- Polling for human-agent takeover messages runs every 4s while the chat is mounted, matching the web widget's behavior.
- Conversation history is persisted locally (
AsyncStorage), mirroring the web widget'slocalStoragecache, so a returning user sees their prior messages.
example/ is a minimal, runnable Expo app demonstrating all integration styles side
by side.
npm install && npm run build # build the SDK once
cd example
npm install
npm startThen press i / a / w to try it on iOS Simulator, Android emulator, or web.
- React 18+, React Native 0.72+ (Expo SDK 49+)
@react-native-async-storage/async-storage1.19+
Contributing — bug reports, design-parity fixes, and PRs are welcome.
Licensed under MIT © PersonaliAI