> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gethinto.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Install the React Native SDK

> Install @gethinto/react-native in an Expo or bare React Native app, start capture, and follow screens and taps.

`@gethinto/react-native` does for a mobile app what the [browser SDK](/browser-sdk) does for a web app. It records what the customer did (screens, taps, errors, console, network, leaving and returning to the app, identity and journeys), and it runs the in-app support conversation and Hinto Signal help offers. Both SDKs share one core, so a version behaves the same on the web and on a phone.

The SDK is pure JavaScript. It runs in Expo Go, in development builds and in bare React Native apps, and an SDK fix reaches your users with your next over-the-air update. It needs React Native 0.82 or later with Hermes, or Expo SDK 55 or later.

## What you need first

The **publishable key** of your organization's Signal app. Your website and your iOS and Android apps share one Signal app and one publishable key. It is in Hinto under Settings > Hinto Signal, it can only send session data and open support, and it is safe to ship in the app. Store it as `EXPO_PUBLIC_HINTO_PUBLISHABLE_KEY` and pass it as `apiKey`.

* **Already have Signal on your website?** Reuse the same publishable key and backend values. Your backend's token endpoint already signs users, so the app calls that same endpoint. Do not pair and do not create anything in Hinto.
* **No backend, and customers sign in with Supabase, Firebase, Clerk, Auth0, or another OpenID provider?** The app passes that provider's token, as [Support identity without a backend](#support-identity-without-a-backend) shows. When a coding agent pairs, it sends `identity_provider` too.
* **No Signal values anywhere you can read?** A coding agent pairs as in [Set up with a coding agent](/native-support#set-up-with-a-coding-agent), steps 3 to 5, with `"origins": []`. If the claim answers `existing`, the organization already has its app: write `publishableKey`, and reuse the backend values its website's backend already has, as step 4 says. Without an agent, an admin chooses **Set up without a coding agent** in Settings > Hinto Signal.

A mobile app needs no entry in the websites list: Hinto checks an origin only for requests from a browser. The app appears under Products in Settings > Hinto Signal after its first session. A debug build (`__DEV__`) shows as Local and a release build as Live, so testing on a simulator never marks Signal live.

## Install

```bash theme={null}
npx expo install @gethinto/react-native@^0.9.2 @react-native-async-storage/async-storage expo-crypto expo-application expo-device
```

In a bare React Native app, use `npm i @gethinto/react-native@^0.9.2` and the same peers, or `react-native-device-info` and `react-native-get-random-values` in place of the Expo modules. Each peer is optional, and the SDK works without it in a stated way:

| Peer | Gives | Without it |
| - | - | - |
| `@react-native-async-storage/async-storage` | Unsent events and a pending message survive a kill | Memory only. Events not yet sent and a pending support message are lost when the app is closed. Logs a warning in development. |
| `expo-crypto` or `react-native-get-random-values` | Random ids | `Math.random`, with a warning in development. |
| `expo-application` and `expo-device`, or `react-native-device-info` | App version and build, device model, TestFlight | The OS and its version, and the `appVersion` option. |
| `@react-native-community/netinfo` | Online and offline changes | The app counts as online. A failed send waits, and it is retried when the app returns to the foreground. |

None of them adds native code that the SDK itself needs, so the SDK never ties a fix to a store release.

## Start capture

Start capture once, as early as the app starts, and wrap the root in `HintoProvider` so Hinto can record taps:

```tsx theme={null}
import { HintoProvider, initHinto } from "@gethinto/react-native";

void initHinto({ apiKey: process.env.EXPO_PUBLIC_HINTO_PUBLISHABLE_KEY ?? "" });

export default function RootLayout() {
  return <HintoProvider>{/* your app */}</HintoProvider>;
}
```

`initHinto` resolves with the client once the SDK has read its stored state and any events the last run left unsent. A second call stops the first client. Off iOS and Android, such as when Expo renders your routes for the web or for static output, the client records and sends nothing, so a module-level call is safe there.

`HintoProvider` renders one full-size `View` that watches touches without becoming a touch responder, so it never takes a touch from your components. It records a tap, not a scroll or a long press, with the tapped control's label: `hintoLabel` first, then `accessibilityLabel`, then `testID`. It never records the text on screen. Put `hintoIgnore` on a view whose subtree Hinto must not label.

```tsx theme={null}
<Pressable hintoLabel="Save book" onPress={save} />
```

`hintoLabel` and `hintoIgnore` are props Hinto reads from the component tree. TypeScript may need them declared on your own components.

## Follow screens

Pass the navigation container's ref, and every event then carries the screen's templated route, such as `book/[id]` or `Settings/Profile`, never its params:

```tsx theme={null}
import { useNavigationContainerRef } from "expo-router";
import { useTrackNavigation } from "@gethinto/react-native";

export default function RootLayout() {
  useTrackNavigation(useNavigationContainerRef());
  return <HintoProvider>{/* your app */}</HintoProvider>;
}
```

With React Navigation, call `trackNavigation(navigationRef)` once the container is ready, with the ref from `createNavigationContainerRef()`. An app without either still records everything else, with no screen on its events.

## Sessions on a phone

A session starts when the app starts cold, when the customer comes back after more than five minutes away, and after four hours or 10,000 events, so one session never spans days. Leaving and returning are recorded with the time away, so a case file reads "Left the app, came back after 7 minutes, then the error".

Events wait in a small queue on the device, capped at 512 KB, and go out every 5 seconds and when the app goes to the background. iOS suspends an app seconds after it leaves the screen, so what is not sent then goes out at the next start, under the session it belongs to.

When Hinto refuses the publishable key, capture stops until the app starts again. Every request gives up after 30 seconds and is retried later.

## Configuration

Every option except `apiKey` is optional.

| Option | Default | What it does |
| - | - | - |
| `apiUrl` | `https://api.gethinto.com` | The Hinto API to send to. |
| `ingestUrl` | `apiUrl` + `/v1/ingest` | A full ingest endpoint, when it is not the default. |
| `initialIdentity` | none | `{ kind: "external_id", value: user.id }` for the signed-in customer. Journeys and support need it. |
| `appVersion` | from `expo-application` | Your release, when no peer can read it. |
| `environment` | none | Your deployment name, such as `production` or `staging`. |
| `captureConsoleLevels` | `["warn", "error"]` | Console levels to record. It chains with any console patch already installed. `[]` records none. |
| `captureTaps` | `true` | Records taps reported by `HintoProvider`. |
| `captureNetwork` | `true` | Records each request's method, redacted URL, status and duration, once each. Requests to Hinto are never recorded. |
| `captureUnhandledRejections` | `false` | Records unhandled promise rejections. Off by default: Hermes has one rejection tracker, and taking it switches off Sentry's or Bugsnag's. |
| `allowUrlSearchParams` | none | Query parameter names to keep in captured request URLs. See [Privacy controls](/privacy). |
| `redactPatterns` | built-in patterns | Replaces the redaction patterns. See [Privacy controls](/privacy). |
| `flushIntervalMs` | `5000` | Milliseconds between sends. |
| `debug` | `false` | `true` logs `[Hinto]` warnings. A function receives each line instead. |

Uncaught errors are always recorded, chained to the handler that was already set, so React Native's own error screen, Sentry and Bugsnag still see them. A release build's stack is bytecode offsets until it meets the build's source maps, so Hinto marks it as unsymbolicated. Run Sentry for readable native crashes and stacks: Hinto reads Sentry as evidence and lines its events up with the session by app version and time.

## Client methods

The client has the same methods as the browser SDK's: `identify`, `journey`, `track`, `flush` and `shutdown`. See [Install the browser SDK](/browser-sdk#client-methods). Call `identify(null)` at sign-out: it starts a new session and erases the queued events and journeys of the person who signed out, so they never attach to the next person on a shared device.

## Support in the app

`connectConversation` from this package starts the support conversation, exactly as on the web, and rejects with a `ConversationConnectError` when Hinto cannot resolve the app. It returns a `ConversationRuntime` with the browser SDK's methods; an attachment is a `{ uri, name, type, size }` file from your picker. `createConversation` builds one from a config you already resolved. Start capture with `initHinto` first: the runtime runs on the same client. Pass the publishable key as `ingestKey`. The app sends no origin: the signed token is what Hinto checks. It comes from one of two places:

* **Your sign-in provider.** Most apps without a backend. See [Support identity without a backend](#support-identity-without-a-backend).
* **Your backend.** It signs the support token for the signed-in user, the same way it does for the web. The token's `aud` is `HINTO_SIGNAL_APP_ID`, the Signal app ID shown under Backend in Settings > Hinto Signal (`applicationId` when you paired). See [Issue a token on your authenticated server](/native-support#issue-a-token-on-your-authenticated-server).

See [security and identity](/native-support-security) for what Hinto checks.

### Support identity without a backend

When customers sign in with Supabase, Firebase, Clerk, Auth0, or another OpenID provider, name that provider once in Hinto: as `identity_provider` when a coding agent pairs, or under **Sign-in provider** in Settings > Hinto Signal. The app then passes the provider's own token, and needs no backend and no backend values. [Use your sign-in provider's token](/native-support#use-your-sign-in-providers-token) lists what to enter for each provider and what Hinto checks.

With Supabase, enter `{"kind":"supabase","url":"https://<ref>.supabase.co"}`, the URL the app's Supabase client uses. Pass the session's user ID as `subject`, and its access token from `getToken`. Hinto calls `getToken` with `true` after a 401, so refresh the session then. Throw when nobody is signed in:

```ts theme={null}
import { supabase } from "./supabase";

export async function supabaseSupportToken(forceRefresh: boolean): Promise<string> {
  const { data } = forceRefresh
    ? await supabase.auth.refreshSession()
    : await supabase.auth.getSession();
  if (!data.session) throw new Error("Support authentication required");
  return data.session.access_token;
}

// subject is session.user.id, the same ID capture's initialIdentity uses.
// <SupportProvider auth={session ? { subject: session.user.id, getToken: supabaseSupportToken } : null} ... />
```

The Supabase project must sign with JWT signing keys, the default for new projects. A project on the legacy JWT secret must migrate in the Supabase dashboard under JWT Keys first. The other providers work the same way: Firebase passes `user.uid` and `user.getIdToken(forceRefresh)`, Clerk passes `userId` and `getToken({ skipCache: forceRefresh })` from `useAuth()`, and Auth0 passes `user.sub` and an access token requested for the API audience you entered.

A phone is usually closed when a teammate replies. Set a reply webhook in Settings > Hinto Signal, under Reply notifications, and send a push from your backend when it fires. Without one, the Inbox tells your team the customer sees the reply the next time they open the app.

## The support screen

`@gethinto/react-native-support` draws the conversation and help offers on this runtime, with the same states, wording and rules as the web kit. It needs a newer app than the SDK: React Native 0.86 or later, which is Expo SDK 57 or later. The SDK itself still runs on Expo SDK 55. Unlike the SDK, the support kit is not pure JavaScript: it needs native libraries for the keyboard, its animation, the safe area and its icons, at the versions Expo SDK 57 ships (`react-native-keyboard-controller` 1.21.9, `react-native-reanimated` 4.5, `react-native-safe-area-context` 5.7 and `react-native-svg` 15.15.4) or later. Every export of the kit loads all four, `SupportProvider` and `HelpOffer` included, so install all of them even when you use only part of the kit. Expo's install command picks the versions that match your Expo SDK. If your app does not have them yet, make a new development build after you install them. The kit is not supported in Expo Go: use a development build.

```bash theme={null}
npx expo install @gethinto/react-native-support@^0.1.2 react-native-keyboard-controller react-native-reanimated react-native-worklets react-native-safe-area-context react-native-svg
```

Wrap your app's root in `KeyboardProvider` from `react-native-keyboard-controller`, and in `SafeAreaProvider` from `react-native-safe-area-context` (Expo Router already adds it). Without `KeyboardProvider` the composer does not move with the keyboard, and `react-native-keyboard-controller` logs a warning in development.

```tsx theme={null}
import { HelpOffer, SupportConversation, SupportProvider } from "@gethinto/react-native-support";
import { KeyboardProvider } from "react-native-keyboard-controller";

<KeyboardProvider>
  <SupportProvider
    capture={capture}
    ingestKey={publishableKey}
    auth={user ? { subject: user.id, getToken: fetchSupportToken } : null}
    onReauthenticate={() => router.push("/sign-in")}
    theme={{
      colors: { accent: "#5b21b6" },
      fonts: { regular: "Inter-Regular", semibold: "Inter-SemiBold" },
    }}
  >
    <SupportConversation pickAttachment={pickFromLibrary} />
  </SupportProvider>
</KeyboardProvider>;
```

* `SupportProvider` connects once a customer is signed in, follows sign-outs and account switches without reconnecting, and destroys the runtime on unmount. `useSupport` reads its state, and `useSupportEntry` gives the unread count for a "Chat with us" row.
* `SupportConversation` is the whole conversation: the status steps, every state's notice and actions, the transcript and the composer. Give it the whole screen, and do not pad it for the status bar, the home indicator or the keyboard: its header keeps clear of the status bar and the Dynamic Island, and it keeps the composer above the home indicator and moves it with the keyboard itself. If your screen draws its own bar above the conversation, such as a navigation header, pass `topSafeArea={false}`. Show it on a screen without a tab bar, such as a stack screen or a modal above your tabs: inside a tab navigator the tab bar covers the bottom edge, and the composer would sit a tab bar's height away from the keyboard. `headerActions` puts your controls at the end of its header, such as a close button for a modal. `style` styles its outer view, and `testID` names it for your tests. `pickAttachment` is your own picker, returning `{ uri, name, type, size }` files, or null when the customer cancels. If the picker throws, the conversation tells the customer their files could not be opened and logs `[Hinto] pickAttachment failed` with the error. Without `pickAttachment` there is no attach button. Hinto accepts PNG, JPEG, GIF, WebP, HEIC and HEIF images, PDF, plain text, CSV and JSON, up to 10 MB each (`ATTACHMENT_TYPES` and `MAX_ATTACHMENT_BYTES`), and refuses other files before they upload. iPhone photos can go straight from the picker: Hinto stores a HEIC or HEIF photo as a JPEG named `.jpg`.
* `HelpOffer` shows the Signal offer for one journey, with Contact support and Not now. It counts as shown only once it is on screen, which it measures itself (`isOnScreen`); inside a scrolling list, pass `visible` from the list's viewability callback. `onContactSupport` opens your support screen.
* `useSupportConversation` and `useHelpOffer` give the same behavior as data, for a design you draw yourself, and `createSupportController` is the controller under them. `SupportConversationView` draws the stock conversation from the controls `useSupportConversation` returns, so you can wrap an action before it runs. It takes the same `headerActions`, `topSafeArea`, `style` and `testID`, and `dateFormat` (`{ locale, timeZone }`) for message times and day separators, which `SupportConversation` reads from `SupportProvider`. Without `dateFormat` it shows no times. `selectView` and `projectView` turn a runtime state into one of `SUPPORT_VIEW_KINDS`, with the `REQUIRED_ACTIONS` each kind must offer.
* Theme with `theme`: `colorScheme` picks `lightTheme` or `darkTheme` as the base, `colors` replaces single colors, and `fonts` names one font family per weight (`regular`, `medium`, `semibold`), as `resolveTheme` merges them. Name each weight's font file as you loaded it: the kit then never sets a font weight on it. A weight you leave out uses the next lighter one. Without fonts the system font is used. Spacing, sizes and the type scale are fixed. Every control keeps a `MIN_TOUCH_TARGET` of 44 points, and text grows with the system text size up to 1.6 times. Translate with `copy`, which overrides `defaultCopy` key by key, as `resolveCopy` merges it, using the web kit's keys. A localized app passes every key, including the function keys such as `openAttachment(filename)`: each key it leaves out shows in English.

## Limits

The support conversation shares the web's limits, exported as `MAX_MESSAGE_LENGTH`, `MAX_ATTACHMENTS_PER_MESSAGE`, `MAX_ATTACHMENT_BYTES`, `ATTACHMENT_TYPES` (with `attachmentTypeFor` to type a file), `MAX_HANDOFF_TURNS`, `MAX_HANDOFF_TURN_LENGTH`, `CONTACT_QUEUE_TTL_MS` and `SUPPORT_STATUS_PHASES`. `isSupportReply` tells a teammate's reply from the customer's own messages.

## Verify it works

Start the app with `debug: true`, sign in, and move between two screens. The Metro console shows `[Hinto] ingest response: accepted N, duplicates N, rejected N`. `rejected 0` means Hinto took every event. In Hinto, Sessions shows the session with the phone's model, OS and app version, and its screens. Remove `debug: true` afterwards.

## Related guides

* [Privacy controls](/privacy)
* [Journeys](/journeys)
* [Identify users](/identify)
* [Hinto Signal](/native-support)
* [Troubleshooting](/troubleshooting)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.