Add the Chat to Your Mobile App
Your Mojeeb agent's chat also works inside your own iOS or Android app. The app opens the chat page in a WebView with your widget's id, and people chat with the same agent as on your website. You need no API key, SDK or native library from Mojeeb. This guide includes a React Native component to copy.
How does the chat work inside an app?
The chat is a web page, https://chat.mojeeb.app/?widgetId=YOUR_WIDGET_ID&deviceType=mobile: the same chat your website widget opens, so any WebView can load it. It answers with your agent, and it tells your app when it's ready to show and when the visitor taps its ✕ to close it.
Your app decides when to show and hide the chat. Everything inside it works as on your website: text, photos, files, voice notes, the visitor's earlier conversations and live replies. In an app, the chat has its default look, a black header titled "Support team", rather than your widget's colour.
Where do I find my widget id?
In the widget code from your dashboard, or from the Public API. Copy the code from Website on the Connect page, as you would for a website (see Embed on Your Website), and use its data-widget-id value. Your app needs only that id, not the code itself.
Through the API, GET /v1/agents/{agentId}/widget returns the same widget, and its id is the widget id; see Website Widget and the Get an agent's website widget reference. Make that call from your server or a script, never from the app: it needs an API key.
The widget id isn't a secret. It's in the page source of every website that has your widget, and it only lets people chat with your agent.
Which address does the app load?
https://chat.mojeeb.app/?widgetId=YOUR_WIDGET_ID&deviceType=mobile, with your widget's id in place of YOUR_WIDGET_ID. Keep deviceType=mobile so the chat's keyboard behaves as on a phone. Add initialMessage to open the chat with a line from your agent, and encode it with encodeURIComponent.
| Parameter | Required | What it does |
|---|---|---|
widgetId | Yes | Your widget's id. Conversations go to its agent. |
deviceType | Yes, as mobile | The Return key adds a new line and the send button sends, as on a phone. Without it, Return sends the message. |
initialMessage | No | URL-encoded text the chat shows as the agent's opening line, in this chat window only. |
For example: https://chat.mojeeb.app/?widgetId=YOUR_WIDGET_ID&deviceType=mobile&initialMessage=Need%20help%20with%20your%20order%3F
How do I add the chat to a React Native app?
Install react-native-webview and react-native-safe-area-context, copy the MojeebChat component below into your app, and render it once with your widget id. Set open to true to show the chat. The component calls onClose when the visitor taps the chat's ✕ or Android's back button, and your app then sets open back to false.
npx expo install react-native-webview react-native-safe-area-context
Without Expo, install both packages with your package manager, then run pod install in your ios folder.
import { useEffect, useRef, useState } from 'react';
import { BackHandler, KeyboardAvoidingView, Linking, Platform, Pressable, StyleSheet, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WebView } from 'react-native-webview';
const CHAT_URL = 'https://chat.mojeeb.app/';
// The chat posts to window.parent, which in a WebView is the page itself: forward to React Native.
const FORWARD_MESSAGES = `
window.addEventListener('message', function (e) {
var type = e.data && e.data.type;
if (type === 'widgetReady' || type === 'closeWidget') window.ReactNativeWebView.postMessage(type);
});
true;
`;
type Props = { widgetId: string; open: boolean; onClose: () => void; message?: string };
export function MojeebChat({ widgetId, open, onClose, message }: Props) {
const webView = useRef<WebView>(null);
const [url, setUrl] = useState('');
const [ready, setReady] = useState(false);
// Load on first open; after that, reload only to show a different opening message.
useEffect(() => {
if (!open) return;
let next = `${CHAT_URL}?widgetId=${encodeURIComponent(widgetId)}&deviceType=mobile`;
if (message) next += `&initialMessage=${encodeURIComponent(message)}`;
if (!url || (message && next !== url)) setUrl(next);
}, [open, widgetId, message]);
// Tell the chat whether it can be seen: it goes quiet while hidden and catches up when shown.
useEffect(() => {
if (!ready) return;
const type = open ? 'widgetShown' : 'widgetHidden';
webView.current?.injectJavaScript(`window.postMessage({ type: '${type}' }, '*'); true;`);
}, [open, ready]);
// Android's back button closes the chat.
useEffect(() => {
if (!open) return;
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
onClose();
return true;
});
return () => sub.remove();
}, [open, onClose]);
return (
// Hidden with opacity, not display: 'none', which would drop the WebView and reload the chat.
<SafeAreaView
style={[StyleSheet.absoluteFill, styles.overlay, !open && styles.hidden]}
pointerEvents={open ? 'auto' : 'none'}
accessibilityElementsHidden={!open}
importantForAccessibility={open ? 'auto' : 'no-hide-descendants'}
>
{/* iOS: shrink the chat above the keyboard, or the page scrolls its header off screen. */}
<KeyboardAvoidingView style={styles.fill} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
{!!url && (
<WebView
ref={webView}
source={{ uri: url }}
style={[styles.fill, !ready && styles.hidden]}
injectedJavaScriptBeforeContentLoaded={FORWARD_MESSAGES}
onMessage={(e) => {
if (e.nativeEvent.data === 'widgetReady') setReady(true);
if (e.nativeEvent.data === 'closeWidget') onClose();
}}
onLoadStart={() => setReady(false)}
onContentProcessDidTerminate={() => webView.current?.reload()}
mediaCapturePermissionGrantType="grant"
allowsInlineMediaPlayback
scrollEnabled={false}
bounces={false}
hideKeyboardAccessoryView
onOpenWindow={(e) => Linking.openURL(e.nativeEvent.targetUrl)}
onShouldStartLoadWithRequest={(req) => {
if (!req.isTopFrame || !/^https?:/i.test(req.url) || req.url.startsWith(CHAT_URL)) return true;
Linking.openURL(req.url);
return false;
}}
/>
)}
{!ready && (
<Pressable onPress={onClose} style={styles.close} accessibilityRole="button" accessibilityLabel="Close chat">
<Text style={styles.closeText}>✕</Text>
</Pressable>
)}
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
overlay: { backgroundColor: '#fff', zIndex: 10 },
hidden: { opacity: 0 },
fill: { flex: 1 },
close: { position: 'absolute', top: 8, right: 8, padding: 12 },
closeText: { fontSize: 20, color: '#555' },
});
Then render it after your app's content, so it covers the screen when open:
import { useCallback, useState } from 'react';
import { Button, View } from 'react-native';
import { MojeebChat } from './MojeebChat';
export default function App() {
const [chatOpen, setChatOpen] = useState(false);
const closeChat = useCallback(() => setChatOpen(false), []);
return (
<View style={{ flex: 1 }}>
{/* ...your app... */}
<Button title="Chat with us" onPress={() => setChatOpen(true)} />
<MojeebChat widgetId="YOUR_WIDGET_ID" open={chatOpen} onClose={closeChat} />
</View>
);
}
Put MojeebChat where it stays mounted, such as your root layout: unmounting it throws the chat away, and the next open loads it from scratch. Until the chat is ready, the component shows a plain white background with its own ✕, so the visitor can always close it. The code is TypeScript; for JavaScript, delete the Props type, : Props and <WebView>.
How do I open the chat with an opening message?
Pass message to the MojeebChat component, which adds it to the address as initialMessage. The chat shows it as the first line from your agent, for example "Need help with order #1042?" when opened from an order screen. It appears only in this chat window: it isn't saved in the conversation, so neither your team nor the agent's AI sees it.
It works like MojeebWidget.open({ message }) on a website. Because the AI doesn't see the opening line, write it as a greeting, not as information the agent needs. MojeebChat reloads the chat only when the message changes; opening it again without a message keeps the chat as it was.
Which messages does the chat send to my app?
Two, each an object with a type: widgetReady once the chat has painted, and closeWidget when the visitor taps the chat's ✕. Show the chat on the first and hide it on the second. The page posts them with window.parent.postMessage, which in a WebView reaches the page itself, so your app must forward them.
type | Sent when | What your app does |
|---|---|---|
widgetReady | The chat has painted, after every load | Shows the WebView |
closeWidget | The visitor tapped the chat's ✕ | Hides the chat |
A WebView loads the chat as its top-level page, so window.parent is the chat's own window. Inject a listener before the page loads that passes both messages to native code. In React Native, that's injectedJavaScriptBeforeContentLoaded, which forwards them with window.ReactNativeWebView.postMessage to the WebView's onMessage, as FORWARD_MESSAGES does in MojeebChat.
Which messages can my app send to the chat?
Two optional ones, for apps that keep the chat loaded while it's hidden: widgetHidden when you hide it, and widgetShown when you show it again. While hidden, the chat goes quiet; when shown, it catches up on anything it missed. Send them by running this in the chat's page, as MojeebChat does with injectJavaScript:
window.postMessage({ type: 'widgetHidden' }, '*'); // when you hide the chat
window.postMessage({ type: 'widgetShown' }, '*'); // when you show it again
An app that destroys the WebView when the chat closes needs neither: it loads a fresh chat every time.
Why hide the chat instead of closing it?
To keep it warm. A hidden chat stays loaded, so the next open is instant and shows the conversation where the visitor left it. MojeebChat hides it with opacity: 0 and pointerEvents="none". In React Native, don't use display: 'none': the new architecture drops the native views of a display: 'none' subtree, so the chat would reload on every open.
MojeebChat also hides the chat from screen readers while it's closed, and sends widgetHidden and widgetShown so the chat knows whether it can be seen.
How does the chat stay on screen when the keyboard opens?
On iOS, wrap the WebView in a KeyboardAvoidingView with behavior="padding", as MojeebChat does, so the chat shrinks to fit above the keyboard. Without it, iOS scrolls the whole page up while the visitor types, and the chat's header, with its title and ✕, leaves the screen. Turn off the WebView's own scrolling too.
In MojeebChat, the WebView sets scrollEnabled={false} and bounces={false}, because the chat scrolls its own message list, and hideKeyboardAccessoryView, which removes the bar iOS adds above the keyboard.
How do links and attachments open?
In the phone's browser. Links in the agent's replies and attachments open in a new window (target="_blank"), which a WebView ignores unless your app handles it. In MojeebChat, onOpenWindow passes them to Linking.openURL, and onShouldStartLoadWithRequest sends any other page the chat would navigate to there too, so the chat never leaves its WebView.
onShouldStartLoadWithRequest lets through every load within the chat's address, along with loads in frames other than the top one and addresses that aren't http or https.
What happens when the chat reloads?
MojeebChat hides the WebView from the start of every load until the chat sends widgetReady, so the visitor sees a plain background with a ✕ rather than a half-loaded page. iOS can end a hidden WebView's process to free memory; onContentProcessDidTerminate then reloads the chat, and the visitor's conversation comes back with it.
Which permissions does my app need?
The microphone for voice notes, and the camera and photo library for photos. Declare them as for any other feature of your app; the system asks the visitor the first time the chat needs one. On iOS, mediaCapturePermissionGrantType="grant" (iOS 15 and later) stops WebKit from asking for the microphone a second time after the system has.
| Platform | Declare | For |
|---|---|---|
iOS (Info.plist) | NSMicrophoneUsageDescription | Voice notes |
iOS (Info.plist) | NSCameraUsageDescription | Taking a photo |
iOS (Info.plist) | NSPhotoLibraryUsageDescription | Choosing a photo |
Android (AndroidManifest.xml) | android.permission.RECORD_AUDIO | Voice notes |
Android (AndroidManifest.xml) | android.permission.CAMERA | Taking a photo |
With Expo, set them in app.json, the iOS texts under ios.infoPlist and the Android permissions under android.permissions:
{
"expo": {
"ios": {
"infoPlist": {
"NSMicrophoneUsageDescription": "Record voice notes in the support chat.",
"NSCameraUsageDescription": "Take photos to send in the support chat.",
"NSPhotoLibraryUsageDescription": "Choose photos to send in the support chat."
}
},
"android": {
"permissions": ["android.permission.RECORD_AUDIO", "android.permission.CAMERA"]
}
}
}
Expo Go includes react-native-webview, so you can try the chat there, but Expo Go has its own permissions. Yours apply in a development build or a store build of your app, made after you change them.
Common questions
Can I pass the app user's name, email or id to the chat?
No. Every chat from an app starts as an anonymous guest, as on a website, and the chat page takes no details about the user. If the visitor shares their name, phone or email in the conversation, the agent can capture it as a lead, as it does on your website.
Does the conversation survive an app restart?
Yes. The chat keeps the visitor's guest identity in the WebView's storage, so after a restart the same conversation continues. Logging out of your app doesn't reset it, because the chat knows nothing of your app's accounts: on a shared phone, the next person sees the same chat. Clearing the WebView's stored data starts a new guest.
Will my app get a push notification when the agent replies?
No. The chat sends no push notifications. While the chat is open, replies from the agent and your team appear live. If the visitor has left the app, the reply is waiting in the conversation the next time they open the chat.
Where do app chats appear in the dashboard?
In Chats, with the conversations from your website widget, under the Website platform filter. The same agent answers them with the same knowledge, your team can reply and take over as on any website chat, and each visitor also appears on the Contacts page.
Do I need an API key?
No. The app needs only the widget id, which isn't a secret: it's in the page source of every website with your widget, and it only lets people chat with your agent. Never put a Mojeeb API key in an app. Anyone can extract it from the app, and a key can change your agents and read your contacts.
Which plan do I need?
Any plan. The app uses your website widget, which every plan includes, and needs no API key, so the Public API, which needs a paid plan, isn't required. The agent's replies in app chats count toward your plan's monthly messages, as replies on your website do. See Plans & Pricing.
Can I change the chat's colour from the app?
No. In an app, the chat has its default look, a black header titled "Support team", whatever Primary Color you set in the dashboard. That setting applies to the chat button and chat on your website.
Does the dashboard's Initial Message show in the app?
No. The Initial Message you set in the dashboard belongs to the bubble beside your website's chat button, and an app has no chat button. To greet people in your app, pass your own opening line in the initialMessage parameter, which the message prop of MojeebChat sets. It shows in that chat window only.
Does it work with Flutter, or in native iOS and Android apps?
Yes. The chat is a web page, so any WebView can show it; this guide shows React Native. In any framework, do the same things: load the chat address, forward widgetReady and closeWidget to your code, open links in the browser, declare the microphone and camera permissions, and hide the WebView rather than destroying it.