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.
On this page
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.
npx expo install @giovchat/react-native react-native-safe-area-contextnpm install @giovchat/react-native react-native-safe-area-contextWith 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)
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:
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.
<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}
/>userHashis the HMAC-SHA256 (lowercase hex) ofuserId, 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 for server examples.- Without
userHash,userIdis recorded as an unverified claim and doesn't link the chat to an existing contact. - If the workspace requires verified identity and
userHashis missing or wrong, the chat reportsidentity_rejectedthroughonError. - Sign-out and account switching: change
sessionKey, or callref.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 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:
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.
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.
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,
}));
},
};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.
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
- 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.
- Register each device token with the SDK:
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 orsessionKeychanges first, onreset(), 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
localeprop) 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 }, undergiovon APNs and indataon 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:
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.
<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, andautofollows the device. Override it withtheme. - 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.
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).