# React Native and Expo

> Add the Giov chat to a React Native or Expo app as a modal or a full screen, with verified identity, attachments, unread counts and push notifications.

The React Native SDK (`@giovchat/react-native`, version 0.2) shows the same conversation your website visitors have as a native screen. It needs no native modules. Optional features such as file picking and push notifications use your app's own libraries through small adapters.

Colours, greeting, pre-chat fields, GiovBot's name and avatar, attachment rules and branding come from **Settings → Chat widget** in the dashboard. In code, `tenantKey` is your workspace ID, shown in **Settings → Chat widget → Installation**.

## Install

Requires React Native 0.72 or newer and `react-native-safe-area-context` 4 or newer.

```bash title="Terminal" tab="Expo"
npx expo install @giovchat/react-native react-native-safe-area-context
```

```bash title="Terminal" tab="React Native"
npm install @giovchat/react-native react-native-safe-area-context
```

With pnpm, Yarn or Bun, use their `add` command instead. Wrap your app in `SafeAreaProvider` from `react-native-safe-area-context` if your navigation setup doesn't already provide one. The package is plain JavaScript (CommonJS) with TypeScript types, so Metro and Jest load it without extra configuration.

## Show the chat

### Modal (default)

```tsx title="SupportButton.tsx"
import { useState } from "react";
import { Button, View } from "react-native";
import { GiovChat } from "@giovchat/react-native";

export function SupportButton() {
  const [open, setOpen] = useState(false);
  return (
    <View>
      <Button title="Chat with us" onPress={() => setOpen(true)} />
      <GiovChat tenantKey="YOUR_WORKSPACE_ID" visible={open} onClose={() => setOpen(false)} />
    </View>
  );
}
```

### Full screen

The chat fills its parent view, for example a dedicated screen in your navigator:

```tsx title="ChatScreen.tsx"
import { GiovChat } from "@giovchat/react-native";

export function ChatScreen() {
  return <GiovChat tenantKey="YOUR_WORKSPACE_ID" mode="fullscreen" />;
}
```

A conversation starts when the visitor sends their first message or submits the pre-chat form. A stored conversation is resumed first; a closed one is forgotten, and the next message starts a new one.

## Signed-in users

Pass the user's details through `contact`. Any field you provide skips the matching pre-chat field.

```tsx title="Chat.tsx"
<GiovChat
  tenantKey="YOUR_WORKSPACE_ID"
  contact={{
    userId: currentUser.id,
    userHash: currentUser.giovUserHash, // computed on your server
    email: currentUser.email,
    firstName: currentUser.firstName,
  }}
  sessionKey={currentUser.id}
/>
```

- `userHash` is the HMAC-SHA256 (lowercase hex) of `userId`, keyed with the identity secret from **Settings → Chat widget → Visitor details**. Compute it on your server and send it to the app; never ship the secret. See [Identify visitors](/docs/widget/identify-visitors) for server examples.
- Without `userHash`, `userId` is recorded as an unverified claim and doesn't link the chat to an existing contact.
- If the workspace requires verified identity and `userHash` is missing or wrong, the chat reports `identity_rejected` through `onError`.
- **Sign-out and account switching:** change `sessionKey`, or call `ref.current.reset()`. The previous user's session, queued messages and unread count are dropped, and the next user starts fresh. If you registered a push token, [unregister it](#push-notifications) first.

### Keep the conversation across launches

By default nothing is stored. Pass `sessionStorage` to resume the same conversation next time, using secure storage you already have:

```tsx title="chatStorage.ts"
import * as SecureStore from "expo-secure-store";
import type { GiovChatSessionStorage } from "@giovchat/react-native";

const key = (scope: string) => "giovchat." + scope.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 200);

export const chatStorage: GiovChatSessionStorage = {
  async load(scope) { const value = await SecureStore.getItemAsync(key(scope)); return value ? JSON.parse(value) : null; },
  async save(scope, session) { await SecureStore.setItemAsync(key(scope), JSON.stringify(session)); },
  async remove(scope) { await SecureStore.deleteItemAsync(key(scope)); },
};
```

The `scope` the SDK passes identifies the workspace, the `userId` and the `sessionKey`, so a user who edits their name or email keeps their conversation, and each user on a shared device has their own.

> [!WARNING]
> The session token is a private credential. Keep it in secure storage, never log it, and never share it between users.

## Attachments

Attachments appear once the conversation has started, when you pass an `attachmentPicker` and the published GiovBot agent allows them. Visitors can pick only the allowed types and sizes, up to 5 files per message, and the server checks again.

The picker returns `{ uri, name, type, size?, width?, height?, thumbnail? }` objects, or `[]` when the visitor cancels.

```tsx title="pickers.ts" tab="Expo"
import * as DocumentPicker from "expo-document-picker";
import type { GiovChatAttachmentPicker } from "@giovchat/react-native";

export const documentPicker: GiovChatAttachmentPicker = {
  async pick({ allowedTypes }) {
    const result = await DocumentPicker.getDocumentAsync({ type: allowedTypes, multiple: true, copyToCacheDirectory: true });
    if (result.canceled) return [];
    return result.assets.map((asset) => ({
      uri: asset.uri, name: asset.name, type: asset.mimeType ?? "application/octet-stream", size: asset.size,
    }));
  },
};
```

```tsx title="pickers.ts" tab="React Native"
import { pick, types } from "@react-native-documents/picker";
import type { GiovChatAttachmentPicker } from "@giovchat/react-native";

export const filePicker: GiovChatAttachmentPicker = {
  async pick({ allowedTypes }) {
    try {
      const files = await pick({ allowMultiSelection: true, type: allowedTypes.length ? allowedTypes : [types.allFiles] });
      return files.map((file) => ({ uri: file.uri, name: file.name ?? "file", type: file.type ?? "application/octet-stream", size: file.size ?? undefined }));
    } catch {
      return []; // Cancelled.
    }
  },
};
```

Then pass it: `<GiovChat tenantKey="YOUR_WORKSPACE_ID" attachmentPicker={documentPicker} />`. Uploads show progress in the composer.

To let everyone, including your team, see an image's shape before it loads, add a small `thumbnail`: resize the image to about 24 px wide, save it as JPEG in base64, and pass `"data:image/jpeg;base64," + base64`. It must be under 6,000 characters; otherwise it's left out and the image loads in place.

## Unread replies and badges

By default the chat connects only while it's visible. To count replies that arrive while it's closed, keep it mounted with `keepConnected`. It only resumes an existing conversation and never starts one in the background.

```tsx title="ChatTab.tsx"
import { GiovChat, useGiovChatUnreadCount } from "@giovchat/react-native";

function ChatTabIcon() {
  const unread = useGiovChatUnreadCount("YOUR_WORKSPACE_ID");
  return <Icon name="chat" badge={unread || undefined} />;
}

<GiovChat tenantKey="YOUR_WORKSPACE_ID" visible={open} keepConnected sessionStorage={chatStorage}
  onUnreadCountChange={(count) => console.log(count)} />;
```

Opening the chat clears the count.

## Push notifications

1. Add your Apple push key and/or Firebase service account in **Settings → Chat widget → Push notifications**. Notifications are sent from your app, with your keys. See [Push credentials](/docs/mobile#push-credentials).
2. Register each device token with the SDK:

```tsx title="push.ts"
const chat = useRef<GiovChatHandle>(null);
// After your app has an APNs or FCM token (for example from expo-notifications getDevicePushTokenAsync):
const result = await chat.current?.registerPushToken(deviceToken, Platform.OS === "ios" ? "ios" : "android", {
  environment: __DEV__ ? "sandbox" : "production", // APNs only: development builds get sandbox tokens
});
```

- Pass the raw device token: the APNs token on iOS (or the FCM token if your iOS app uses Firebase Messaging), and the FCM token on Android.
- It registers straight away when connected, otherwise as soon as the conversation connects, and never throws. Called before the visitor has written anything, it waits for the first message to start the conversation. It resolves `{ registered: false, reason: "no_conversation" }` if the chat unmounts or `sessionKey` changes first, on `reset()`, or when the workspace refuses the visitor's identity.
- The token is remembered. When your team closes a conversation and the visitor starts a new one, the device is registered for the new one automatically.
- The device language (or the `locale` prop) goes with the token, so **You have a new message**, used when the workspace hides message text, arrives in the visitor's language. Pass `{ locale }` to choose another.
- A tapped notification carries `{ type: "giov.message", tenantId, conversationId, messageId }`, under `giov` on APNs and in `data` on FCM. Open the chat and it catches up from history.

When the visitor signs out or turns notifications off, unregister the token before resetting the chat. This works while the chat is closed too:

```tsx title="signOut.ts"
await chat.current?.unregisterPushToken(deviceToken);
await chat.current?.reset();
```

## Connection and app lifecycle

- The chat stays connected for 5 seconds after your app goes to the background, so a file picker or a glance at the app switcher doesn't reload it. After that it disconnects, and replies arrive as push notifications.
- When the app returns, or a dropped connection comes back, it reconnects and fetches what it missed. Messages typed offline are sent then, in order. **Connection lost. Reconnecting…** shows meanwhile.
- If the first connection fails, for example on a flaky network, the chat keeps retrying on its own with increasing delays. **Try again** retries at once.

## Language

The interface follows the device language: English, French, Spanish, Portuguese or German, with English for others. Text you write in the dashboard (greeting, placeholder, bot name) is shown as written.

```tsx title="Chat.tsx"
<GiovChat tenantKey="YOUR_WORKSPACE_ID" locale="pt-BR" strings={{ sendMessage: "Enviar", startChat: "Começar" }} />
```

Replace any string through `strings`; `GiovChatStrings` lists the keys and `GIOVCHAT_STRINGS` exports the built-in tables.

## Appearance

- **Theme:** the dashboard setting (`light`, `dark`, `auto`) applies, and `auto` follows the device. Override it with `theme`.
- **Colour:** the dashboard's main colour applies. Override it with `primaryColor`. Text on that colour turns black or white, whichever is easier to read.
- **Fonts and surface colours:** `styleOverrides`.
- **Privacy link:** the header menu links to the **Privacy policy URL** from **Settings → Chat widget → Appearance**, as the web widget does.

```tsx title="chatStyles.ts"
import { GiovChat, type GiovChatStyleOverrides } from "@giovchat/react-native";

const chatStyles = {
  fontFamily: "YourAppFont", // Load/register this font in your app first.
  container: { backgroundColor: "#f8fafc" },
  header: { backgroundColor: "#0f172a" },
  headerTitle: { color: "#ffffff", fontWeight: "700" },
  headerSubtitle: { color: "#cbd5e1" },
  agentBubble: { backgroundColor: "#e2e8f0" },
  agentText: { color: "#0f172a" },
  visitorBubble: { backgroundColor: "#2563eb" },
  visitorText: { color: "#ffffff" },
  composer: { backgroundColor: "#f8fafc" },
  input: { backgroundColor: "#ffffff", color: "#0f172a" },
  button: { backgroundColor: "#2563eb" },
  buttonText: { color: "#ffffff" },
} satisfies GiovChatStyleOverrides;

<GiovChat tenantKey="YOUR_WORKSPACE_ID" styleOverrides={chatStyles} />;
```

| Slots | Allowed properties | Applies to |
|---|---|---|
| `container`, `header`, `composer`, `form` | `backgroundColor` | Chat surface, header, composer row, pre-chat form |
| `agentBubble`, `visitorBubble` | `backgroundColor` | Agent, greeting and typing bubbles; visitor bubbles |
| `button` | `backgroundColor` | Send, start-chat and retry buttons |
| `headerTitle`, `headerSubtitle` | `fontFamily`, `fontWeight`, `fontStyle`, `color` | Header text; the title colour also applies to the close icon |
| `agentText`, `visitorText` | Same text properties | Message content, sender labels and timestamps; agent text also covers greeting and typing |
| `formTitle`, `label`, `buttonText`, `hint`, `errorText` | Same text properties | Form heading and labels, button text, loading hint, attachment chips, errors; the hint colour also sets input placeholders |
| `input` | Text properties plus `backgroundColor` | Composer and all pre-chat inputs |

A slot's `fontFamily` takes precedence over the top-level one. Unsupported slots and properties are ignored. Font sizes, spacing, borders, positioning and the branding footer stay under the SDK's control.

**Branding:** Starter and Pro always show **Powered by GiovChat**. Business and Advanced can turn it off in **Settings → Chat widget → Appearance**. The footer's look isn't affected by style overrides.

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `tenantKey` | `string` | **required** | Your workspace ID |
| `mode` | `"modal" \| "fullscreen"` | `"modal"` | Modal sheet, or fill the parent view |
| `visible` | `boolean` | `true` | Show or hide the chat |
| `onClose` | `() => void` | — | Called when the visitor taps close |
| `contact` | `GiovChatContact` | — | Signed-in user's details |
| `sessionKey` | `string \| number` | — | Change it to isolate or reset the session (sign-out, account switch) |
| `sessionStorage` | `GiovChatSessionStorage` | none | Your secure storage for the session |
| `baseUrl` | `string` | `https://api.giovchat.app` | API base URL override |
| `primaryColor` | `string` | From dashboard | Brand colour override |
| `theme` | `"light" \| "dark" \| "auto"` | From dashboard | Theme override; `auto` follows the device |
| `styleOverrides` | `GiovChatStyleOverrides` | none | Allowed font and colour overrides |
| `locale` | `string` | Device language | Interface language |
| `strings` | `Partial<GiovChatStrings>` | none | Replace any interface string |
| `attachmentPicker` | `GiovChatAttachmentPicker` | none | Enables attachments, when the agent allows them |
| `keepConnected` | `boolean` | `false` | Stay connected while hidden to count unread replies |
| `onUnreadCountChange` | `(count: number) => void` | — | Unread replies while hidden |
| `onConversationStart` | `(id: string) => void` | — | Called once with the conversation ID |
| `onError` | `(error: GiovChatError) => void` | — | Every failure, with a `code` |
| `onConnectionStatusChange` | `(status) => void` | — | `"connecting"`, `"connected"` or `"disconnected"` |

The `ref` (`GiovChatHandle`) has `reset()`, which clears this user's session and starts fresh, `registerPushToken(token, platform, options?)` and `unregisterPushToken(token)`.

`onError` codes: `network`, `timeout`, `invalid_response`, `invalid_session` (the stored session is gone), `conversation_closed`, `connection_failed`, `disconnected`, `send_failed`, `upload_failed`, `workspace_busy`, `identity_rejected` (the workspace requires a verified identity and `userHash` is wrong or missing), `storage`, `server` and `rate_limited` (with `retryAfterSeconds`).
