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:
| Capability | Adapter |
|---|---|
| Video layers | expo-video |
| In-app review buttons | expo-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-videoOr 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:
- Add the
react-native-permissionsconfig plugin inapp.config.ts/app.json. - Set
ios.infoPlistusage descriptions andandroid.permissionsfor every capability you ship in flows. - Run
npx expo prebuildwhen regenerating native projects, then rebuild the dev client.
permissionKey | Native capability |
|---|---|
notifications | requestNotifications |
camera | CAMERA |
microphone | RECORD_AUDIO |
photo_library | Photo library / READ_MEDIA_IMAGES |
contacts | READ_CONTACTS |
calendar | Calendar 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
};| Field | API / behavior |
|---|---|
publishableKey | Required. Bearer token for resolve and events. |
apiBaseUrl | Defaults to https://api.getrheo.io. |
userId | identity.appUserId on every event. Set explicitly on React Native. |
customUserId | identity.customUserId — update at runtime via useRheoCustomUserId(). |
sessionId | identity.sessionId |
locale | Resolve cache key + manifest locale selection |
appVersion | context.appVersion |
platform | Override ios / android / web inference |
sdkAttributes | Merged into decision evaluation (sdk.* refs) |
attribution | AppsFlyer adapter config — see AppsFlyer |
customProperties | Arbitrary string map on events |
fetcher | Optional 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
<Flow /> (recommended)
Fully managed: resolve, loading spinner, LayerRenderer, external surfaces, events, terminal callbacks.
Flow props:
| Prop | Type | Default | Description |
|---|---|---|---|
channelId | string | — | Required. Channel public id. |
theme | 'light' | 'dark' | 'light' | Screen chrome palette. |
fallback | ReactNode | built-in retry UI | Full-bleed UI when resolve fails. No Rheo telemetry on fallback. |
onFlowCompleted | (FlowTerminalSnapshot) => void | — | Terminal success handler. |
onFlowAbandoned | (FlowTerminalSnapshot) => void | — | Explicit or unmount abandonment. |
includeManifestInTerminalPayload | boolean | false | Add manifest to snapshot. |
includePathInTerminalPayload | boolean | false | Add visited path ids. |
includeAnswerDetailInTerminalPayload | boolean | false | Add raw answersDetail. |
withGestureRoot | boolean | true | Wrap in gesture root for carousel/drag. |
locale | string | 'en' | Manifest locale (also affects cache key). |
onOAuthLogin | handler | — | OAuth layer callback. |
onEmailPasswordAuth | handler | — | Email/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:
| Member | Description |
|---|---|
loading | Resolve in flight |
error | Last resolve error |
resolveFailed | !loading && error && !manifest |
retry() | Re-run resolve |
state | FlowState from runtime |
screen | Current Screen |
manifest | Full FlowManifest |
pendingExternalSurface | Active paywall node, if any |
flowId, versionId, variantId | Assignment metadata |
branding | Colors, fonts, gradients |
mediaMap | Asset id → CDN URL |
respond(StepResponse) | Submit step input |
relayNativeButtonAction | Button / permission / review actions |
trackExternalLinkOpened | Hyperlink 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
presentRevenueCatPaywallwhenreact-native-purchases-uiis installed. Override viauseFlow({ externalSurfacePresenter }). - AppsFlyer — optional peer; normalized keys merge into
sdkAttributesafter 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:
| Export | Kind | Purpose |
|---|---|---|
RheoProvider | component | Root context + event queue; optional prefetch, logLevel |
useRheo | hook | Access config + queue |
useRheoCustomUserId | hook | Runtime CRM id |
useEventQueue | hook | Event queue (advanced) |
Flow | component | Managed flow UI |
useFlow | hook | Headless flow controller |
LayerRenderer | component | Layer tree renderer |
ScreenChrome | component | Screen shell chrome |
DefaultResolveError | component | Default retry UI |
MotionProvider, LayerMotionShell | components | Animation context |
prefetch, prefetchAll, useRheoPrefetch | functions / hook | Manifest warmup |
OAuthLoginProvider, useOAuthLogin | provider / hook | OAuth host bridge |
EmailPasswordAuthProvider, useEmailPasswordAuth | provider / hook | Email/password bridge |
presentRevenueCatPaywall, normalizeRcPaywallResult | functions | Paywall helper |
buildBrandingFontLoadMap | function | Font loading map |
FlowTerminalSnapshot, FlowTerminalSnapshotSchema | type / zod | Terminal payload |
RheoConfig, RheoAttributionConfig | types | Provider config |
SdkLogLevel, DEFAULT_SDK_LOG_LEVEL | type / const | Diagnostics verbosity (silent | warn | debug) |
UseFlowOptions, UseFlowResult, FlowProps | types | Flow APIs |
RheoChannel*Error | errors | Typed resolve failures |
clearManifestResolveCache, … | functions | Cache introspection |
Flow runtime re-exports: initFlowState, submitResponse, buildCompletionResponses, stepResponseToCompletionValue, manifest types from @getrheo/contracts.
HTTP API (SDK → Rheo)
| Endpoint | Method | Headers | Body |
|---|---|---|---|
/v1/sdk/resolve | POST | Authorization: Bearer <publishableKey>, X-Rheo-Channel: <channelId> | Identity + If-None-Match when cached |
/v1/sdk/resolve-all | POST | Authorization: Bearer <publishableKey> | Batch prefetch |
/v1/sdk/events | POST | Authorization, X-Rheo-Channel per batch | Event array |