<!-- AI agents: this is the Markdown version of a Reception documentation page. Index: /llms.txt -->

# React Native

Add Reception’s native iOS support chat to a React Native or Expo app. The official Swift SDK handles the chat, photos, replies, and storage; your JavaScript connects it to your app.

Requires Expo SDK 54+ (verified 54–57), with its default React Native and React versions, iOS 17+, Xcode 26, and Expo Modules. Expo Go cannot load Reception; use a development build.

## 1. Install {#install}

```sh
npx expo install react-native-reception
```

Add the plugin to your existing app config, preserving other plugins:

```json app.json
{
  "expo": {
    "plugins": ["react-native-reception"]
  }
}
```

For generated native projects, apply plugins even when `ios/` already exists, then build:

```sh
npx expo prebuild --platform ios
npx expo run:ios
```

## 2. Configure {#configure}

Configure once in your existing async startup function, before identity or chat. Keep Support disabled until it completes and handle failures with your app’s error UI. Copy your public App ID from **Settings → your app → General**:

```typescript
import { Reception } from 'react-native-reception';

// Reception support chat. Docs for changes: https://reception.sh/llms.txt
await Reception.configure({ appId: 'app_YOUR_APP_ID' });
```

Methods return promises to run native work on the main queue. Await configuration before reading `Reception.isConfigured`; this checks accepted configuration, not connectivity. Registration begins with the first valid message send.

## 3. Open chat {#open-chat}

Copy your app’s existing button style and call `Reception.openChat()` on press:

```tsx
import { Button } from 'react-native';
import { Reception } from 'react-native-reception';

// Inside your screen; use your own button component and styling.
<Button title="Support" onPress={() => void Reception.openChat()} />;
```

`await Reception.closeChat()` dismisses the sheet. Your app owns the Support entry and its [unread badge](#unread).

## Appearance and language {#appearance}

Read accentColor, onAccentColor, chatBackground, incomingBackground, incomingForeground and fontFamily from matching values in the app’s existing theme. Leave unmatched options unset; never invent colors. Pass both light and dark theme colors so the native chat follows the app’s color scheme. Assign a complete local appearance object. Omitted fields restore Swift defaults; invalid fields log and retain their previous value:

```typescript
// theme is your app’s existing theme; use only fields it actually provides.
Reception.appearance = {
  accentColor: { light: theme.light.accent, dark: theme.dark.accent },
  onAccentColor: { light: theme.light.onAccent, dark: theme.dark.onAccent },
};
// Only if your app has its own language picker:
Reception.languageOverride = selectedLanguage;
```

Each color accepts `#RRGGBB`, `#RRGGBBAA`, or `{ light, dark }` with both values in either hex format. This applies to all six colors, including `welcomeIconColor`. Pairs follow the native color scheme without JavaScript updates. Enable automatic appearance in your app config if your app follows the system. The 23 options correspond to the [Swift appearance options](/docs/sdk-reference#appearance-properties): colors, title and welcome copy, close button, light/dark mode, font design/family, older-message fading, welcome icon size/position/rounding, and text visibility. Reassign when the app’s own theme selection changes; mutating a nested property does not update native styling.

Use `closeIcon: 'xmark'`, `'chevron.down'`, or `'arrow.down'`; `fontDesign` accepts `default`, `rounded`, `serif`, or `monospaced`. `preferredColorScheme` accepts `light`, `dark`, or `null`. `welcomeIcon` accepts `{ symbol: 'star' }`, `{ image: 'Welcome' }`, `{ hidden: true }`, or `null`. `closeImage` and welcome images use names from the host iOS asset catalog. `fontFamily` is a registered iOS font name.

Published dashboard appearance overrides local options by default. Set `Reception.usesRemoteAppearance = false` for local-only styling. Set `languageOverride = null` to follow the app’s selected iOS localization. Existing messages and your supplied text are not translated.

Setting getters reflect valid assignments immediately, then reconcile with native validation. Live state updates asynchronously from native snapshots, including after a JavaScript reload. Assign settings during startup; await methods when subsequent work depends on completion.

## Entry and unread badges {#unread}

Copy the app’s existing button or settings-row component and styling; localize the label through its existing translation system. In that component, call `Reception.openChat()` on press and use the unread hook for a live count. For example, if the app already has a `SettingsRow` with `title`, `onPress` and `badge` props:

```tsx
import { Reception, useReceptionUnreadCount } from 'react-native-reception';
import { SettingsRow } from './SettingsRow'; // Your existing component.

export function SupportRow() {
  const unread = useReceptionUnreadCount();
  return (
    <SettingsRow
      title="Support"
      onPress={() => void Reception.openChat()}
      badge={unread > 0 ? unread : undefined}
    />
  );
}
```

Adapt props and the translated title to your component. Put the same live badge on every entry and intermediate menu row; badge only the tab with the main entry using your navigator’s badge option. Follow the setup prompt’s badge placement and style rules.

Place hooks inside your own React component. Hide badges at zero:

```typescript
import {
  useReceptionUnreadCount,
  useReceptionChatOpen,
} from 'react-native-reception';

const unread = useReceptionUnreadCount();
const chatOpen = useReceptionChatOpen();
```

Outside React, read `Reception.unreadCount` and `Reception.isChatOpen`, or subscribe with `addUnreadCountListener` and `addChatOpenListener`. Each returns a subscription with `remove()`; remove it when its owner unmounts. Visibility reports actual chat appearance, not just an open request.

`Reception.updatesAppBadge` defaults to `true`. Assign `false` if your app owns the icon badge and notification cleanup. In-app unread counts still work.

## Accounts and user details {#identity}

After configuration, identify the current account and supply the complete metadata dictionary:

```typescript
await Reception.identify({
  userId: 'user_123',
  name: 'Alex',
  email: 'alex@example.com',
});
await Reception.setMetadata({ plan: 'Pro', provider: 'RevenueCat' });
```

These values are staged for the next device update. Metadata values must be strings, at most 20 fields. Each call replaces the whole dictionary; keep one owner and update it whenever chosen fields change. Identification labels this device; it does not authenticate your app’s account or restore another device’s history. Pass `null` for an unavailable user ID. See [Users & accounts](/docs/accounts) for field limits and behavior.

For verified identity, fetch a signed token from your own server, then call `await Reception.identify({ token })`. Never include your identity secret in JavaScript or the app bundle. Fetch a fresh token at signed-in startup. Before supplying an asynchronously fetched token, check that the same account is still signed in; a sign-out or account switch during the request must not attach the old identity. In your existing `Reception.onEvent` handler, refresh only when `event.type === 'identityRejected'` and `event.error.code === 'identity_token_expired'`. Other rejection codes require fixing your server configuration; do not keep retrying them. Preserve other event handling and the logout/deletion hooks. Follow [verified users](/docs/accounts#verified-users) for the server contract; use these JavaScript calls instead of the Swift examples.

Supply a user ID only for a real signed-in account; skip generated guest or anonymous IDs. With an entered name but no sign-in, call `identify({ userId: null, name })` at launch and whenever it changes. Names and emails belong in `identify`, never metadata.

Call `await Reception.logout()` when a real signed-in session ends (sign-out, account switch or an expired restored session), before identifying another user. Never log out a guest at startup or before their first sign-in. Reapply identity, metadata, and the APNs token for the next session. Logout retains server history.

For account deletion, call `deleteData()` before logout and keep the session available on failure:

```typescript
import { Reception, ReceptionError } from 'react-native-reception';

try {
  await Reception.deleteData();
  // Continue your account-deletion flow.
} catch (error) {
  if (error instanceof ReceptionError) {
    // Use error.code and error.status for diagnostics; offer retry.
  }
}
```

Deletion concerns this device’s support data, not your app’s account or other devices. Completion can be local when Reception no longer recognizes the session; see [deletion limits](/docs/accounts#delete-data).

## Push {#push}

Install `expo-notifications` with `npx expo install expo-notifications`, add its config plugin, then run `npx expo prebuild --platform ios` and rebuild generated native projects. For maintained native projects, apply the capability and entitlement steps in [Build and simulator checks](#simulator-checks). Configure Apple push credentials and the matching bundle identifier in [Push notifications](/docs/push-notifications). Keep your existing permission flow; this helper never requests permission.

After Reception is configured, start the optional helper once in your root component and return its cleanup:

```typescript
import { startReceptionPush } from 'react-native-reception/expo-notifications';

useEffect(() => startReceptionPush(), []); // Import useEffect from React.
```

It registers the raw APNs token when already authorized, checks again on foreground and after `logout()` or successful `deleteData()`, forwards token changes, handles foreground receipt and opens chat on taps, including cold starts. It does nothing on Android and web. Remove any older Reception notification listeners to avoid routing twice. Other notification services and their listeners stay in place. The main package import does not load `expo-notifications`.

After your existing permission request completes, call the nonprompting refresh, even if the app stayed in the foreground (for example, after granting provisional permission):

```typescript
import { refreshReceptionPush } from 'react-native-reception/expo-notifications';

await refreshReceptionPush(); // After your app’s permission request; the helper must be started.
```

In your existing notification handler, compose the presentation policy with `receptionNotificationBehavior`. It returns `null` for other notifications, shows Reception banners/sound while chat is closed, and hides them while chat is open. It only selects presentation; the started helper owns routing. For an app with no existing handler:

```typescript
import * as Notifications from 'expo-notifications';
import { receptionNotificationBehavior } from 'react-native-reception/expo-notifications';

Notifications.setNotificationHandler({
  handleNotification: async (notification) =>
    receptionNotificationBehavior(notification) ?? {
      shouldShowBanner: true,
      shouldShowList: true,
      shouldPlaySound: true,
      shouldSetBadge: true, // Replace this fallback with your app's existing policy.
    },
});
```

Reception sends APNs directly. The helper reads the original payload in `request.trigger.payload`, never an Expo token or FCM registration token. It deduplicates receipt and tap callbacks, including retained responses after a JavaScript reload, and never clears Expo’s shared last-response storage. Your app’s notification router should leave Reception routing to this helper.

### React Native Firebase {#firebase-push}

Keep Firebase for your app’s FCM notifications. Reception sends raw APNs without `gcm.message_id`, so Firebase’s `onMessage`, `onNotificationOpenedApp` and `getInitialNotification` do **not** route Reception notifications. Use the `expo-notifications` helper above for Reception (including its APNs registration), or forward raw notifications through your existing native notification-center delegate using the [native push helpers](/docs/native-ios#push).

If you keep APNs-token registration in Firebase with that native delegate, use this after configuration at startup, foreground, after logout/deletion and after the app’s existing permission flow. It never prompts:

```typescript
import { Platform } from 'react-native';
import { Reception } from 'react-native-reception';
import {
  getMessaging,
  getAPNSToken,
  hasPermission,
  AuthorizationStatus,
  registerDeviceForRemoteMessages,
} from '@react-native-firebase/messaging';

async function registerReceptionFirebaseToken() {
  if (Platform.OS !== 'ios') return;
  const messaging = getMessaging();
  const permission = await hasPermission(messaging);
  if (
    permission !== AuthorizationStatus.AUTHORIZED &&
    permission !== AuthorizationStatus.PROVISIONAL &&
    permission !== AuthorizationStatus.EPHEMERAL
  )
    return;
  await registerDeviceForRemoteMessages(messaging);
  const token = await getAPNSToken(messaging);
  if (token) await Reception.setPushToken(token);
}
```

Firebase’s `onTokenRefresh` supplies an FCM token: call this function again instead of forwarding that value. Native APNs callbacks can forward `Data` with `Reception.shared.setPushToken`. Preserve unrelated routing; the native foreground helper hides only Reception banners while chat is open. Do not install both Reception routing paths.

`setPushToken(token, environment)` accepts `automatic` (default), `sandbox`, or `production`; leave automatic unless you know the signing environment. Verify actual APNs delivery on a physical iPhone.

Only if your own JavaScript receives the raw APNs payload and you use neither routing path above, forward it yourself:

```typescript
const handled = await Reception.handlePushNotification(payload, { openChat: true });
```

Pass `openChat: false` for receipt and `true` (the default) for taps, including cold starts. It returns `true` for a Reception payload and `false` for other notifications, which stay with your router. It only routes; suppress Reception banners while `Reception.isChatOpen` in your presentation policy.

## Paywalls {#paywalls}

Use the matching [React Native paywall helper](/docs/paywalls#react-native) for RevenueCat, Superwall, or your own screen. Start it after Reception and your vendor are configured. Reception opens purchase screens your app already owns. Register their stable IDs, then supply your existing presentation and eligibility functions:

```typescript
Reception.onPaywall = (id) => showYourPaywall(id);
Reception.paywalls = [{ id: 'plans', title: 'Plans' }];
Reception.paywallAvailability = async (id) => canShowYourPaywall(id);
```

`showYourPaywall` and `canShowYourPaywall` above are functions from your app. Present the purchase screen above chat; Reception keeps chat open. Titles are for your team in the dashboard, not customers.

Without an availability callback, all registered paywalls are available. A callback filters `Reception.availablePaywalls`; observe changes with `addAvailablePaywallsListener`. Callback failures, timeouts, and a JavaScript reload make pending offers unavailable. Register callbacks again at startup. After subscription changes, call `await Reception.refreshPaywalls({ invalidate: true })`; the new result arrives asynchronously.

Assign `null` to remove a callback. A card needs both an available ID and `onPaywall`. See [Paywall cards](/docs/paywalls) for dashboard use. Card taps report opens, not purchases.

## Embedded chat {#embedded}

After configuration, render the native view in a container with a defined height:

```tsx
import { ReceptionChatView } from 'react-native-reception';

// Inside your screen's render:
<ReceptionChatView style={{ flex: 1 }} />;
```

Your screen owns dismissal. Do not also call `openChat()` for this presentation; `closeChat()` only dismisses the SDK sheet. Unmount the embedded view when leaving that screen.

## Events and diagnostics {#events}

Assign `Reception.onEvent` to receive `{ type }` events: `chatOpened`, `messageSent`, `imageSent`, `reviewOpened`, `paywallOpened`, and `dataDeleted`. `identityRejected` also carries a typed `error` with `code` and `status`. Assignment replaces the previous callback; `null` removes it. The [event meanings](/docs/sdk-reference#reception-event-cases) match Swift.

`Reception.logLevel` accepts `off`, `error`, `info` (default), or `debug`. `Reception.version` reports the bundled Swift SDK version. No custom log handler is exposed. Calls before configuration follow the Swift SDK’s behavior: they log and do not crash.

## Build and simulator checks {#simulator-checks}

The plugin sets the minimum iOS deployment target to 17; report an older app target before changing it. If you already use `expo-build-properties`, set its `ios.deploymentTarget` to `17.0` or higher; Reception reports conflicting values regardless of plugin order. No Podfile edits or `use_frameworks!` are needed. The npm package includes its pinned Swift sources and resources.

For native projects you maintain by hand, do not regenerate them with prebuild. Set iOS 17+ in the app’s Xcode build settings and existing CocoaPods deployment-target configuration, run `pod install` from `ios/`, and rebuild. For push, also add **Push Notifications** in Signing & Capabilities, keeping the signed `aps-environment` entitlement and your notification delegate integration.

An existing bare React Native app needs [Expo Modules installed](https://docs.expo.dev/bare/installing-expo-modules/) and an iOS 17 deployment target. Rebuild after adding Reception.

Android and web have no Reception native code. Calls do nothing and warn once; unread count is zero, chat visibility is false, and the embedded view renders nothing.

Build with `npx expo run:ios --device "<simulator name>"`, open the development server in the app, and keep Metro running. The setup prompt’s `-ReceptionSetupCheck` and `-ReceptionPreview` launch arguments work after JavaScript configures Reception, just as in native iOS. Use a normally signed Debug simulator build.

If macOS automation prevents Expo CLI from opening Simulator, build its generated workspace without launching and start Metro separately. Replace `<App>` with the workspace/scheme name in `ios/` and `<device>` with the simulator UUID:

```sh
xcodebuild -workspace ios/<App>.xcworkspace -scheme <App> -configuration Debug \
  -destination 'platform=iOS Simulator,id=<device>' -derivedDataPath ./build
npx expo start --dev-client --localhost
```

With Metro running, use another terminal to install `build/Build/Products/Debug-iphonesimulator/<App>.app` with `xcrun simctl install <device> <path>`. Open the development server once with `xcrun simctl openurl <device> '<app scheme>://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A8081'`, substituting the scheme from the built app’s Info.plist. Then use the setup prompt’s launch and log commands. Use a simulator UUID when more than one is booted.
