Skip to main content

React Native

Every other guide here is a way of loading one script tag. A phone has none, so this is a real package instead — the same conversation, the same inbox, talking to the same public API the website widget does.

1. Install

Works in Expo and in bare React Native. react and react-native are peer dependencies; AsyncStorage is not a dependency at all — you hand it in, and anything with the same three methods will do.

bash
npm install @helpwing/react-native @react-native-async-storage/async-storage

2. Configure the project

Both values are public — the key is the same pk_… the website snippet carries, and shipping it inside an app bundle is expected. Copy yours from the Chat widget screen of your workspace. The API URL is whichever host serves /widget.js, because that is the host the chat endpoints are routed on.

bash
# .env — both values are public, and both end up in the bundle.
EXPO_PUBLIC_HELPWING_API=https://api.helpwing.app
EXPO_PUBLIC_HELPWING_PROJECT_KEY=pk_your_project_key

Nothing needs adding to the allowed origins list: that list exists to stop the widget being embedded on somebody else's website, and an app sends no origin at all. What identifies a conversation as trustworthy is identity verification, further down.

3. Wrap the app once

tsx
// App.tsx
import AsyncStorage from '@react-native-async-storage/async-storage'
import { HelpwingProvider, SupportLauncher, SupportModal } from '@helpwing/react-native'

export default function App() {
  return (
    <HelpwingProvider
      apiUrl={process.env.EXPO_PUBLIC_HELPWING_API!}
      projectKey={process.env.EXPO_PUBLIC_HELPWING_PROJECT_KEY!}
      storage={AsyncStorage}
    >
      <Navigation />

      {/* The floating button, and the sheet it opens. Both optional. */}
      <SupportLauncher />
      <SupportModal />
    </HelpwingProvider>
  )
}

At the shell, above your navigator — one provider owns the conversation, the polling and the stored token for as long as the app runs. Without storage the conversation lasts only as long as the process, which is why leaving it out warns in development.

4. Or draw it yourself

The launcher and the sheet are conveniences. An app with a Help row in its own settings list wants neither — put SupportChat on a screen and open it however you like. Mounting it is what tells us the visitor is reading.

tsx
// SupportScreen.tsx — the chat as a route of your own
import { SupportChat } from '@helpwing/react-native'

export function SupportScreen() {
  return <SupportChat />
}

// HelpRow.tsx — your own entry point, with the unread badge
import { useSupport } from '@helpwing/react-native'

export function HelpRow() {
  const { unreadCount } = useSupport()

  return (
    <Row
      title="Support"
      badge={unreadCount || undefined}
      onPress={() => navigation.navigate('Support')}
    />
  )
}

Or skip the components entirely: everything is on useSupport(), and the transcript is a plain array.

tsx
const { messages, unreadCount, typing, offline, config, send, retry } = useSupport()

// A message that failed carries the id it was sent with, and retrying is free —
// the server stores one message however many times it arrives.
messages
  .filter(message => message.delivery === 'failed')
  .forEach(message => retry(message.clientMessageId!))

5. Say who the visitor is

tsx
// Wherever your signed-in user lives.
<HelpwingProvider
  apiUrl={API}
  projectKey={KEY}
  storage={AsyncStorage}
  identity={
    user
      ? {
          id: user.id,
          email: user.email,
          name: user.name,
          // Computed by YOUR server. See below.
          userHash: user.supportHash,
          metadata: { plan: user.plan },
        }
      : null
  }
>

Applied whenever it changes, including to a conversation that started before anyone signed in — someone asking a question anonymously and then signing in is the ordinary case, and the conversation moves onto their customer record when they do.

The identity secret never goes in the app

userHash is an HMAC-SHA256 of the user id, keyed with your project's identity secret, and your server computes it. This matters more here than on the web: a web bundle can at least be minified into obscurity, while an app bundle is a zip file anybody can download from the store and read. Fetch the hash from your own API alongside the rest of the signed-in user, the way you would a session token. Identifying users has the full recipe.

tsx
// Signing out on a personal device: the next conversation is nobody's yet.
await support.identify(null)

// Signing out on a shared device: forget this conversation entirely.
await support.reset()

What it does about a phone

Four things the web widget never has to think about, all handled for you — worth knowing because they explain what you will see.

  • It asks every five seconds rather than holding a request open. A socket kept alive across a backgrounded process spends battery to learn things nothing will draw. While the chat is closed it drops to every thirty seconds, so the unread badge stays honest, and while the app is in the background it stops entirely.
  • A message sent twice is stored once. Every send carries an id, so the SDK can retry through a tunnel without your customers seeing double. Opening a conversation is the one exception — a start whose answer was lost is shown as a failed send, and retrying it is the visitor's decision.
  • Unsent messages survive the app being killed. They go out on the next launch, in the order they were written.
  • A reply is not emailed to somebody who is reading it. While the chat is on screen the SDK says so, and the agent's reply is delivered to the app alone. Close it and the same reply is emailed as well, as long as the visitor gave an address.

What it does not do yet

  • Push notifications. Nothing arrives while the app is closed. Ask for an email address — require_email on the widget settings screen — and a reply written while the visitor is away reaches them there, which is the same fallback the website chat has.
  • Attachments from the visitor. No client can send one yet. Files an agent attaches are listed by name.
  • One conversation across two devices. The token belongs to one installation, so a reinstall starts a new conversation.

Checklist

  • One provider, at the shell. Above the navigator, not inside a screen, where every navigation would throw the conversation away.
  • Pass storage. Without it, closing the app loses the conversation.
  • The hash comes from your server. Anything in EXPO_PUBLIC_* is public by definition, and so is everything else in the bundle.
  • Ask for an email address. Until push exists, it is the only way to reach somebody who has closed the app.
NextIdentifying users →Attach conversations to real accounts, and sign the claim so it can be trusted.