# Add the Chat to Your Mobile App

> Show your Mojeeb agent's chat inside your own iOS or Android app by loading https://chat.mojeeb.app with your widget id in a WebView, with a React Native component to copy, an optional opening message, the permissions to declare, and the messages the chat exchanges with your 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](/docs/channels/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](https://dashboard.mojeeb.app/connections) page, as you would for a website (see [Embed on Your Website](/docs/getting-started/embed-on-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](./agents/website-widget) and the [Get an agent's website widget reference](./api-reference/get-widget). 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`.

```bash
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.

```tsx title="MojeebChat.tsx"

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:

```tsx

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`:

```js
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`:

```json title="app.json"
{
  "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](/docs/features/lead-management), 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](https://dashboard.mojeeb.app/conversations), 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](/docs/features/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](./overview), 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](/docs/billing/plans-and-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.
