Rheo documentation
Developer Guide

Expo SDK

Install and integrate @getrheo/react-native-expo — RheoProvider, Flow, prefetch, events, and full API reference for Expo and dev client.

Package

@getrheo/react-native-expo — Expo and Expo dev client entry point. Re-exports @getrheo/react-native-core and registers Expo adapters:

CapabilityAdapter
Video layersexpo-video
In-app review buttonsexpo-store-review

Do not install @getrheo/react-native-bare alongside this package.

Runnable reference: getrheo/rheo-example-expo (public). Full-stack local API testing uses the private Rheo monorepo example via pnpm dev:local:app:react-native.

Install

pnpm add @getrheo/react-native-expo \
  react react-native \
  react-native-permissions react-native-gesture-handler react-native-reanimated \
  react-native-linear-gradient react-native-svg lottie-react-native \
  react-native-vector-icons @react-native-async-storage/async-storage \
  react-native-safe-area-context expo-store-review expo-video

Or with Expo's installer for Expo-managed peers:

npx expo install @getrheo/react-native-expo \
  react-native-gesture-handler react-native-reanimated \
  react-native-safe-area-context expo-store-review expo-video \
  @react-native-async-storage/async-storage lottie-react-native \
  react-native-svg react-native-linear-gradient react-native-vector-icons \
  react-native-permissions

@react-native-community/slider ships as a direct dependency of core (no separate install).

Integrations (not SDK peers): install react-native-appsflyer and/or react-native-purchases + react-native-purchases-ui only when your flows use attribution or RevenueCat paywalls.

Native setup

Root layout

Wrap your app with GestureHandlerRootView and SafeAreaProvider before RheoProvider:

import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { RheoProvider } from '@getrheo/react-native-expo';

const RootLayout = () => (
  <SafeAreaProvider>
    <GestureHandlerRootView style={{ flex: 1 }}>
      <RheoProvider config={rheoConfig}>
        {/* routes */}
      </RheoProvider>
    </GestureHandlerRootView>
  </SafeAreaProvider>
);

OS permissions

Flows can include buttons with action.kind: "request_os_permission". The SDK calls react-native-permissions — no host onOsPermission hook.

Expo checklist:

  1. Add the react-native-permissions config plugin in app.config.ts / app.json.
  2. Set ios.infoPlist usage descriptions and android.permissions for every capability you ship in flows.
  3. Run npx expo prebuild when regenerating native projects, then rebuild the dev client.
permissionKeyNative capability
notificationsrequestNotifications
cameraCAMERA
microphoneRECORD_AUDIO
photo_libraryPhoto library / READ_MEDIA_IMAGES
contactsREAD_CONTACTS
calendarCalendar read access

Missing peer or missing native setup → SDK resolves denied and follows the manifest branch.

Branding fonts

Resolve may return branding.fontFamilies with per-style download URLs. The SDK maps layer fontFamily to RheoFont__{styleId} PostScript names.

import { buildBrandingFontLoadMap } from '@getrheo/react-native-expo';
import * as Font from 'expo-font';

// After resolve (or from a cached manifest):
const fontMap = buildBrandingFontLoadMap(branding);
await Font.loadAsync(fontMap);

Ionicons for icon layers are bundled — no extra setup.

Step-by-step integration

1. Configure credentials

From the dashboard: App settings → SDK credentials (publishable key) and Channels (public channel id). See SDK environment.

2. Create RheoConfig

import type { RheoConfig } from '@getrheo/react-native-expo';

const rheoConfig: RheoConfig = {
  publishableKey: 'ob_pk_test_xxxxxxxx',
  // apiBaseUrl: 'http://localhost:4000', // local dev only
  userId: 'user_stable_id',       // required for RN — analytics + experiment bucketing
  sessionId: `sess_${Date.now()}`, // per app launch
  appVersion: '1.4.0',
  locale: 'en',
  customUserId: 'crm_123',         // optional — your backend user id
  customProperties: { tier: 'pro' },
  sdkAttributes: { onboarding_cohort: '2026-q2' }, // decision node inputs
};
FieldAPI / behavior
publishableKeyRequired. Bearer token for resolve and events.
apiBaseUrlDefaults to https://api.getrheo.io.
userIdidentity.appUserId on every event. Set explicitly on React Native.
customUserIdidentity.customUserId — update at runtime via useRheoCustomUserId().
sessionIdidentity.sessionId
localeResolve cache key + manifest locale selection
appVersioncontext.appVersion
platformOverride ios / android / web inference
sdkAttributesMerged into decision evaluation (sdk.* refs)
attributionAppsFlyer adapter config — see AppsFlyer
customPropertiesArbitrary string map on events
fetcherOptional fetch override for tests

3. Mount RheoProvider

import { RheoProvider } from '@getrheo/react-native-expo';

<RheoProvider config={rheoConfig} prefetch="all">
  <App />
</RheoProvider>

prefetch: "all" | string[] | omit — warms manifest cache on mount. See Manifest prefetch.

logLevel: "silent" | "warn" | "debug" | omit — SDK console diagnostics. Defaults to silent (no logs). Use "warn" when debugging event transport or RevenueCat integration failures; "debug" enables dev-only manifest dumps and misuse hints (requires a dev build). See Diagnostics and logging.

<RheoProvider config={rheoConfig} prefetch="all" logLevel="warn">
  <App />
</RheoProvider>

4. Render a flow

import { Flow } from '@getrheo/react-native-expo';
import type { FlowTerminalSnapshot } from '@getrheo/react-native-expo';

const OnboardingScreen = () => (
  <Flow
    channelId="ch_test_a1b2c3d4"
    theme="dark"
    onFlowCompleted={(payload: FlowTerminalSnapshot) => {
      void fetch('/api/onboarding-complete', {
        method: 'POST',
        body: JSON.stringify(payload),
      });
    }}
    onFlowAbandoned={(payload) => {
      console.log('abandoned', payload.correlation.channelId);
    }}
  />
);

5. Optional — auth and paywalls

<Flow
  channelId="ch_test_…"
  onOAuthLogin={(payload) => {
    signInWithGoogle()
      .then(() => payload.resolve({ success: true }))
      .catch((err) => payload.resolve({ success: false, error: String(err) }));
  }}
  onEmailPasswordAuth={(payload) => {
    signUp(payload.email, payload.password)
      .then(() => payload.resolve({ success: true }))
      .catch((err) => payload.resolve({ success: false, error: String(err) }));
  }}
/>

Details: Authentication layers. RevenueCat: integration guide — default presenter is built into core when react-native-purchases-ui is installed.

Managed renderer vs custom chrome

Fully managed: resolve, loading spinner, LayerRenderer, external surfaces, events, terminal callbacks.

Flow props:

PropTypeDefaultDescription
channelIdstringRequired. Channel public id.
theme'light' | 'dark''light'Screen chrome palette.
fallbackReactNodebuilt-in retry UIFull-bleed UI when resolve fails. No Rheo telemetry on fallback.
onFlowCompleted(FlowTerminalSnapshot) => voidTerminal success handler.
onFlowAbandoned(FlowTerminalSnapshot) => voidExplicit or unmount abandonment.
includeManifestInTerminalPayloadbooleanfalseAdd manifest to snapshot.
includePathInTerminalPayloadbooleanfalseAdd visited path ids.
includeAnswerDetailInTerminalPayloadbooleanfalseAdd raw answersDetail.
withGestureRootbooleantrueWrap in gesture root for carousel/drag.
localestring'en'Manifest locale (also affects cache key).
onOAuthLoginhandlerOAuth layer callback.
onEmailPasswordAuthhandlerEmail/password layer callback.

useFlow + LayerRenderer

import {
  useFlow,
  LayerRenderer,
  ScreenChrome,
  DefaultResolveError,
} from '@getrheo/react-native-expo';

const CustomFlow = ({ channelId }: { channelId: string }) => {
  const {
    loading,
    resolveFailed,
    retry,
    screen,
    manifest,
    state,
    respond,
    branding,
    mediaMap,
    interpolationContext,
    relayNativeButtonAction,
    trackExternalLinkOpened,
    abandon,
  } = useFlow({
    channelId,
    onFlowCompleted: (p) => console.log(p.terminal),
  });

  if (loading) return <ActivityIndicator />;
  if (resolveFailed) return <DefaultResolveError onRetry={retry} />;
  if (!screen || !manifest) return null;

  return (
    <ScreenChrome screen={screen} branding={branding} theme="light">
      <LayerRenderer
        manifest={manifest}
        screen={screen}
        locale="en"
        branding={branding}
        mediaMap={mediaMap}
        interpolationContext={interpolationContext}
        onRespond={respond}
        relayNativeButtonAction={relayNativeButtonAction}
        trackExternalLinkOpened={trackExternalLinkOpened}
        flowState={state}
      />
    </ScreenChrome>
  );
};

useFlow result:

MemberDescription
loadingResolve in flight
errorLast resolve error
resolveFailed!loading && error && !manifest
retry()Re-run resolve
stateFlowState from runtime
screenCurrent Screen
manifestFull FlowManifest
pendingExternalSurfaceActive paywall node, if any
flowId, versionId, variantIdAssignment metadata
brandingColors, fonts, gradients
mediaMapAsset id → CDN URL
respond(StepResponse)Submit step input
relayNativeButtonActionButton / permission / review actions
trackExternalLinkOpenedHyperlink analytics
abandon()Transition to abandoned

Identity at runtime

import { useRheoCustomUserId } from '@getrheo/react-native-expo';

const ProfileLinked = () => {
  const { customUserId, setCustomUserId } = useRheoCustomUserId();

  useEffect(() => {
    if (user?.crmId) setCustomUserId(user.crmId);
  }, [user?.crmId, setCustomUserId]);

  return <Flow channelId="ch_test_…" />;
};

setCustomUserId updates queued events at flush time.

Manifest prefetch

Provider (on mount):

<RheoProvider config={config} prefetch="all">
  {/* warms every assigned channel via POST /v1/sdk/resolve-all */}
</RheoProvider>

<RheoProvider config={config} prefetch={['ch_test_paywall', 'ch_test_onboarding']}>
  {/* selective */}
</RheoProvider>

Imperative (outside React tree):

import { prefetch, prefetchAll, useRheoPrefetch } from '@getrheo/react-native-expo';

// After RheoProvider has mounted once:
await prefetch('ch_test_onboarding');
await prefetchAll();

// Inside a component:
const { prefetch, prefetchAll } = useRheoPrefetch();

Prefetch never records flow_started. A mounted <Flow /> still owns error/retry UI if prefetch failed silently.

Resolve fallback

<Flow
  channelId="ch_test_…"
  fallback={<OfflineOnboarding />}
/>

Without fallback, the SDK shows "Error to load the content" and a Try again button calling retry().

Typed resolve errors: RheoChannelRequiredError, RheoChannelNotFoundError, RheoChannelArchivedError.

Events

Automatic — see Event catalog. You do not call track APIs for standard funnel events.

Optional low-level access: useEventQueue() from the provider context for advanced testing.

External surfaces and attribution

  • RevenueCat — default presentRevenueCatPaywall when react-native-purchases-ui is installed. Override via useFlow({ externalSurfacePresenter }).
  • AppsFlyer — optional peer; normalized keys merge into sdkAttributes after resolve when plan and app integrations allow. AppsFlyer guide.

In-app review

Buttons with action.kind: "request_app_review" use expo-store-review:

npx expo install expo-store-review

~1.5s delay after prompt when iOS may have shown a dialog.

Cache utilities

import {
  clearManifestResolveCache,
  listManifestResolveCacheEntries,
  manifestResolveCacheKey,
} from '@getrheo/react-native-expo';

Full export reference

Import everything from @getrheo/react-native-expo:

ExportKindPurpose
RheoProvidercomponentRoot context + event queue; optional prefetch, logLevel
useRheohookAccess config + queue
useRheoCustomUserIdhookRuntime CRM id
useEventQueuehookEvent queue (advanced)
FlowcomponentManaged flow UI
useFlowhookHeadless flow controller
LayerRenderercomponentLayer tree renderer
ScreenChromecomponentScreen shell chrome
DefaultResolveErrorcomponentDefault retry UI
MotionProvider, LayerMotionShellcomponentsAnimation context
prefetch, prefetchAll, useRheoPrefetchfunctions / hookManifest warmup
OAuthLoginProvider, useOAuthLoginprovider / hookOAuth host bridge
EmailPasswordAuthProvider, useEmailPasswordAuthprovider / hookEmail/password bridge
presentRevenueCatPaywall, normalizeRcPaywallResultfunctionsPaywall helper
buildBrandingFontLoadMapfunctionFont loading map
FlowTerminalSnapshot, FlowTerminalSnapshotSchematype / zodTerminal payload
RheoConfig, RheoAttributionConfigtypesProvider config
SdkLogLevel, DEFAULT_SDK_LOG_LEVELtype / constDiagnostics verbosity (silent | warn | debug)
UseFlowOptions, UseFlowResult, FlowPropstypesFlow APIs
RheoChannel*ErrorerrorsTyped resolve failures
clearManifestResolveCache, …functionsCache introspection

Flow runtime re-exports: initFlowState, submitResponse, buildCompletionResponses, stepResponseToCompletionValue, manifest types from @getrheo/contracts.

HTTP API (SDK → Rheo)

EndpointMethodHeadersBody
/v1/sdk/resolvePOSTAuthorization: Bearer <publishableKey>, X-Rheo-Channel: <channelId>Identity + If-None-Match when cached
/v1/sdk/resolve-allPOSTAuthorization: Bearer <publishableKey>Batch prefetch
/v1/sdk/eventsPOSTAuthorization, X-Rheo-Channel per batchEvent array