# In-app contact with Mocco Messenger

Messenger lets the people signed in to your app write to your team without leaving the app, and lets your team answer from one inbox in Mocco. Each request is a conversation: the user picks a category, writes, and can keep replying in the same thread. Your app version, build and platform come along, so you know what they were running.

## 1. Turn on Messenger

A workspace member turns on **Messenger** on the workspace's **Products** page. Each project then shows an **Inbox** page.

## 2. Set up the messenger

On the project's **Inbox** page, choose **Set up messenger**. Mocco creates the project's **identity secret** and shows it once: copy it and store it as a secret on your server (for example `MOCCO_MESSENGER_SECRET`).

![Setting up the messenger: the identity secret, shown once, and how your server signs a user id](./images/messenger-setup.png)

The secret is how Mocco knows a request really comes from your user. Your server signs each signed-in user's id with it, and your app sends that signature along with the id. Someone who only has your app can't write as another user, because they can't make the signature.

If the secret leaks, choose **Rotate secret** in the inbox settings. Signatures made with the old secret stop working at once, so put the new one on your server right away.

## 3. Create a key for your app

On the **API keys** page, create a **Publishable** key with the **messenger:chat** scope. It is safe to ship in the app: on its own it can't open anything without a signature from your server.

## 4. Sign the user id on your server

Give your app the signature for its signed-in user, for example from an endpoint your app already calls after sign-in:

```ts
import { signIdentity } from '@mocco/node';

// For the signed-in user only.
const userHash = signIdentity(process.env.MOCCO_MESSENGER_SECRET!, user.id);
```

`signIdentity` is HMAC-SHA256 of the user id, as hex. Any language can compute the same value.

## 5. Contact your team from the app

### React Native

Install the SDK (it is plain JavaScript, so it runs in Expo Go too):

```bash
npm install @mocco/react-native @react-native-async-storage/async-storage
```

Create one messenger for the app and wrap the app in its provider. `identity` asks your server for the signed-in user's signature, or returns `null` while nobody is signed in:

```tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createMessenger, MessengerProvider } from '@mocco/react-native/messenger';
import * as Application from 'expo-application';
import { Platform } from 'react-native';

const messenger = createMessenger({
  publishableKey: 'mk_pub_…',
  identity: async () => {
    const user = await getSignedInUser();
    if (user === null) return null;
    const { userHash } = await api.messengerIdentity(); // your endpoint, signs user.id
    return { userId: user.id, userHash, name: user.name, email: user.email };
  },
  context: () => ({
    appVersion: Application.nativeApplicationVersion ?? undefined,
    build: Application.nativeBuildVersion ?? undefined,
    platform: Platform.OS === 'ios' ? 'ios' : 'android',
  }),
  storage: AsyncStorage,
});

export default function App() {
  return (
    <MessengerProvider client={messenger}>
      <Navigation />
    </MessengerProvider>
  );
}
```

Then draw the screens in your own design with the hooks:

```tsx
import { useConversation, useConversations, useMessengerCategories, useUnreadCount } from '@mocco/react-native/messenger';

function SettingsRow() {
  const unread = useUnreadCount(); // a badge on "Contact us"
  // …
}

function ContactList() {
  const { conversations, start } = useConversations(); // refreshed while the screen is open
  const categories = useMessengerCategories();
  // start({ category: 'bug', body }) to open a new one
}

function Thread({ id }: { id: string }) {
  const { messages, send, isSending } = useConversation(id); // new messages are marked read
  // messages: { seq, author: 'contact' | 'operator', authorName, body, createdAt }
}
```

When a user deletes their account in your app, call `useMessenger().deleteMyData()`: it erases their conversations, messages and screenshots in Mocco and forgets them on the device. When someone signs in to your app, call `useMessenger().reidentify()` so the messenger opens their session. When the user signs out of your app, call `useMessenger().signOut()` so the next user starts fresh.

To attach a screenshot (PNG, JPEG, WebP or GIF) or a PDF, up to 10 MB each and up to 3 per message, upload it first and send its id:

```tsx
const messenger = useMessenger();
const picked = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'] });
const asset = picked.assets?.[0];
if (asset !== undefined) {
  // Send bytes: with a Blob, React Native replaces the upload's Content-Type and it is refused.
  const body = new Uint8Array(await (await fetch(asset.uri)).arrayBuffer());
  const attachmentId = await messenger.attach({
    body,
    contentType: 'image/jpeg', // the picked image's real type
    sizeBytes: body.byteLength,
  });
  await send('Here is what I see', [attachmentId]); // from useConversation(id)
}
```

A PDF works the same way with `contentType: 'application/pdf'` (from a document picker, for example). Declare the file's real type: Mocco reads the start of the file, and a message whose file isn't what it claims to be is refused and the file deleted.

Messages come back with `attachments`, each with a `filename`, a `sizeBytes` and a `url` that works for a few minutes. Show an image with `<Image source={{ uri: attachment.url }} />`. A PDF's `url` is a download, so offer it as a file (its name and size) and open it with `Linking.openURL(attachment.url)` or save it, rather than showing it inline.

To get a notification when your team replies while the app is closed, register the device's Expo push token after sign-in, and open the conversation when the user taps the notification:

```tsx
import * as Notifications from 'expo-notifications';
import { messengerConversationIdOf, useMessenger } from '@mocco/react-native/messenger';

const messenger = useMessenger();
const { data: token } = await Notifications.getExpoPushTokenAsync();
await messenger.registerPushToken({ provider: 'expo', token, platform: Platform.OS === 'ios' ? 'ios' : 'android' });

Notifications.addNotificationResponseReceivedListener(response => {
  const conversationId = messengerConversationIdOf(response.notification.request.content.data);
  if (conversationId !== undefined) navigation.navigate('ContactThread', { id: conversationId });
});
```

The notification's title is your project's name and its text is the start of the reply; a reply the user has already read in the app isn't pushed. `signOut()` removes the device, so the next person to sign in on it doesn't get the previous user's replies.

### Any other client

The SDK calls a small HTTP API you can use from anywhere. The base URL is `https://api.mocco.club/v1/messenger`.

```http
POST /sessions
Authorization: Bearer mk_pub_…
Content-Type: application/json

{
  "userId": "user-42",
  "userHash": "4b8d8248…",
  "name": "Minji",
  "email": "minji@example.com",
  "traits": { "plan": "premium" },
  "context": { "appVersion": "3.8.0", "build": "412", "platform": "ios", "locale": "ko-KR" }
}
```

The answer has a `sessionToken` (`mms_…`, valid 30 days) and the project's `categories`. Send the token as `Authorization: Bearer mms_…` on the other calls:

| Call | What it does |
|---|---|
| `GET /conversations` | The user's conversations, newest first. `hasUnread` is true when your team has replied since they last read |
| `POST /conversations` | Start one: `{ "category": "bug", "body": "…", "clientMessageId": "<uuid>", "context": {…} }` |
| `GET /conversations/{id}/messages?afterSeq=0` | The messages, oldest first. Each has a `seq`; pass the last one you have as `afterSeq` to get only new ones |
| `POST /conversations/{id}/messages` | Reply: `{ "body": "…", "clientMessageId": "<uuid>" }` |
| `POST /conversations/{id}/read` | `{ "seq": 5 }` once the user has seen up to that message |
| `POST /push-tokens` | `{ "provider": "expo", "token": "ExponentPushToken[…]", "platform": "ios" }` so replies are pushed to the device; `DELETE /push-tokens` `{ "token": … }` on sign out |
| `DELETE /me` | Erase the user and everything they wrote, for your app's "delete my account" |
| `POST /attachments` | `{ "contentType": "image/png", "sizeBytes": 48213 }` (or `application/pdf`) → an `attachmentId` and an `upload` URL to `PUT` the bytes to; then list the id in `attachmentIds` when you start or reply |

Generate a new `clientMessageId` for each message and reuse it if you retry: Mocco stores the message once, however many times the request arrives. A user can send 20 messages a minute and start 5 conversations an hour.

### People who aren't signed in

To let anyone contact you, including people who haven't signed in, turn on **Let people who aren't signed in write** in the inbox settings. They leave an email so you can reach them. In the app, when the messenger's state is `signed_out`, ask for an email and call `continueAsGuest`:

```tsx
const { status } = useMessengerState();
const messenger = useMessenger();

if (status === 'signed_out') {
  // show an email field, then:
  await messenger.continueAsGuest({ email, name });
}
```

The device remembers the guest, so they see your replies the next time they open the app. If they sign in later on the same device and your app calls `reidentify()`, what they wrote moves to their account. In the inbox a guest's conversation is marked **Guest**, with **Not signed in** and their email beside it. Mocco doesn't email them for you yet; reply in the app, or write to the email they left.

![A guest's conversation: the "Not signed in" badge and the email they left](./images/messenger-guest.png)

## 6. Answer from the inbox

The **Inbox** page lists open conversations, newest activity first, with unread ones marked. **Closed** lists the rest. The inbox checks for new messages every few seconds while it is open.

![The inbox: one open conversation from Minji, a bug report](./images/messenger-inbox.png)

Open a conversation to read the thread and answer:

- **Reply** sends a message the user sees in the app.
- **Internal note** is for your team only; the user never sees it.
- **Attach** adds up to three images (PNG, JPEG, WebP or GIF) or PDFs, 10 MB each, to a reply or a note.
- **Close** when it is resolved. If the user writes again, it reopens by itself.
- The side panel shows who the user is (as your app described them), the app version and platform they are on now, and what they were running when the conversation started.
- **Block user** stops someone from writing. They can still read what they have.

![A conversation: the user's messages, an internal note, the team's replies, and the user panel](./images/messenger-conversation.png)

Screenshots the user attached show in the thread; select one to open it full size.

![A message with an attached screenshot in the conversation view](./images/messenger-attachment.png)

A PDF shows as a file with its name and size. Select it to download it; Mocco never opens a user's PDF in the browser, because a PDF can carry script.

![A message with an attached PDF: the file's name and size, ready to download](./images/messenger-attachment-pdf.png)

To send the user a file, choose **Attach** below the reply, pick the images or PDFs, and select **Send reply**. Each file shows under the text with its size; select **×** to take one off before sending.

![Attaching a screenshot and a PDF receipt to a reply](./images/messenger-reply-attach-composer.png)

The reply shows the files in the thread, and the user's app receives them with the message, the same way it receives the files users send you: images to show, PDFs to download. Files on an internal note stay with your team.

![The sent reply with its screenshot and PDF](./images/messenger-reply-attachments.png)

Mocco checks your files the way it checks the user's: a file that isn't really the image or PDF it claims to be is refused, and nothing is sent.

When someone asks you to delete their data, choose **Erase user's data** in the side panel and confirm. Mocco deletes the user with every conversation, message and attachment; it can't be undone, and the audit log records only that it happened.

![Erasing a user's data: the confirmation in the side panel](./images/messenger-erase.png)

The categories users pick from are in the inbox settings. Your app gets them with each session; it can show the labels as they are or translate them by key.

### Share the work with round robin

Mocco can give each new conversation to one person on your team, in turn, so the work spreads evenly and everyone knows which ones are theirs. Each conversation in the inbox shows who it is assigned to, or **Unassigned**.

- Choose **Join the rotation** at the top of the inbox to start getting new conversations. To add someone else, pick them under **Round robin** in the inbox settings; anyone in the workspace can be added.
- The switch at the top of the inbox is your availability. While you are away, new conversations skip you and go to the next available person. When you come back, the next one is yours.
- If no one is available, new conversations stay unassigned until someone picks them up.
- To hand a conversation to someone else, or take one that is unassigned, pick a person in **Assigned to** at the top of the conversation. Anyone in the workspace can take one, whether or not they are in the rotation; choose **No one** to put it back. Each change is in the audit log.
- Removing someone from the rotation doesn't take away the conversations they already have.

![Round robin: the availability switch over the inbox, each conversation's assignee, and the rotation in the settings](./images/messenger-round-robin.png)

![A conversation's header with the Assigned to picker set to a team member](./images/messenger-assign.png)

## 7. Get notified

New conversations (`messenger.conversation.created`) and users writing again (`messenger.message.received`) are Mocco events. Apply the **Mocco** preset on a Discord channel's rules, or add rules for these two events, to hear about them where your team already is (see [Mocco events](../notifications/mocco-events.md#the-mocco-preset)). A new conversation's message says who it was assigned to.

If you only want to hear about the conversations no one picked up, add a rule for `messenger.conversation.unassigned` instead: Mocco sends it when a conversation starts while nobody in the rotation is available. Each conversation event reaches a channel once, even if the app retries.

While the inbox is open in a browser tab, Mocco can also show a desktop notification for each new conversation. Choose **Turn on desktop notifications** at the top of the inbox and allow it in the browser; clicking a notification opens the conversation. They stop when you close the tab.
