RevenueCat paywall
Present a host-owned RevenueCat paywall as a step in your onboarding flow and branch on the outcome.
Purpose
Add a RevenueCat paywall as a node inside a Rheo flow. The Rheo SDK presents whatever RevenueCat's paywall UI renders, listens for the outcome, and branches the flow accordingly. Your app stays the source of truth for the RevenueCat integration — Rheo never calls Purchases.configure.
How to
-
Install the host packages in your app:
pnpm add react-native-purchases react-native-purchases-ui -
Configure RevenueCat at app startup with your own SDK key. Rheo never configures
Purchasesfor you:import Purchases from 'react-native-purchases'; Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_RC_KEY! }); -
Define an offering (and optionally a placement) in the RevenueCat dashboard. Copy the identifier.
-
Enable the integration in Rheo: App Settings → Integrations → RevenueCat, then save a default offering ID. The toggle is required before the canvas exposes the node.
-
Add a RevenueCat paywall node to a flow from the canvas add menu. Connect each outcome handle (or rely on Fallback).
-
Publish the flow and run it through the SDK — the host triggers
<Flow>oruseFlowexactly as before; the paywall presents automatically when the flow reaches the node.
Normalized outcomes
The flow engine speaks one of four outcomes per paywall step. RevenueCat events are mapped by the SDK so authors never see provider-specific event names:
| Outcome | When it fires |
|---|---|
purchase_completed | The user completed a purchase from the paywall. |
restore_completed | A restore unlocked a new entitlement. Restores with no entitlement land on dismissed / failed. |
dismissed | The user closed the paywall, or the modal was not presented (e.g. presentPaywallIfNeeded opted out). |
failed | RevenueCat reported an error, OR the host did not install react-native-purchases-ui. |
Every paywall node must wire a Fallback edge. It is used for any outcome you do not explicitly map, and the builder blocks publish until it is connected.
Reserved SDK attribute keys
On every outcome, the SDK merges the following keys into the flow's session.sdkAttributes. You can reference these from decision nodes via the existing sdk variable kind without listing them in sdkAttributeKeys — they are auto-allowed.
| Key | Set when | Notes |
|---|---|---|
onb_rc_last_event | Always | purchase_completed / restore_completed / dismissed / failed. |
onb_rc_last_offering_id | The node configured an offering id | Copied from the manifest node config. |
onb_rc_last_product_id | A successful purchase exposed a product id | Read from RevenueCat customer info after purchase_completed. |
onb_rc_last_period_type | RevenueCat supplied a period type | normal / intro / trial as observed. |
Example: branch after the paywall on whether the user purchased.
Decision: "Did the user purchase?"
variable: sdk.onb_rc_last_event
predicate: string eq "purchase_completed"
onTrue: scr_premium_onboarding
onFalse: scr_continue_freeExpo support
The RevenueCat native modules require a custom dev client / prebuild. They cannot run inside stock Expo Go.
- Run
npx expo prebuild(or maintain a managed dev client) soreact-native-purchasesis linked. - Expo Go users see the paywall step resolve as
failed(the lazy require returnsnull); your Fallback edge takes over so the rest of the flow stays usable while developing.
Multi-channel behaviour
useFlow is scoped per channel, and so is the surface's pending state. Multiple channels running on the same device handle their own paywall lifecycles independently. RevenueCat's UI is modal, so two concurrent presentations on the same screen rely on the OS to serialise — Rheo does not install a global lock. In practice, design your channels so only one onboarding surface is on-screen at a time.
Limits and permissions
- Host owns RevenueCat: Rheo does not install
Purchasesfor you. Ifreact-native-purchases-uiis missing at runtime, the paywall step resolves asfailedand a warning is logged. - No server-side receipt validation: outcomes are client-reported. If you need server truth, run your own webhook + entitlement reconciliation separately.
- Late purchases: if a purchase completes after the user already advanced past the paywall (e.g. backgrounded App Store purchase), the outcome is ignored. Design your flow assuming the paywall completes before navigation.
- Branching on entitlement state is intentionally not modeled by Rheo. Use your existing custom user attributes (passed via
sdkAttributeson<RheoProvider>) if you need that.
Analytics
Three analytics events are emitted automatically:
surface_presented— when the paywall is shownsurface_outcome— on every resolve, includingfailediap_purchase— only onpurchase_completed, with product and optional price metadata
See Event catalog for property details.
Advanced: custom presenter
You can override the presenter on a per-useFlow basis for tests or custom UI:
useFlow({
channelId: 'ch_test_xyz',
externalSurfacePresenter: async () => ({ outcome: 'purchase_completed' }),
});Troubleshooting
- Paywall never appears: check that
react-native-purchases-uiis installed in your host app and thatPurchases.configure(...)ran. Logs warn when the paywall UI module is missing. - Manifest fails to publish with integration disabled: the app's RevenueCat integration is off. Toggle it on in App Settings → Integrations and re-publish.
- Manifest fails to save with "Fallback": connect the paywall node's Fallback handle to a real screen (or decision/surface).
- Restore does not branch as expected: RevenueCat fires
restore_completedonly when the restore unlocks a new entitlement; otherwise the outcome isdismissed(orfailedon error). Map both branches explicitly if you need different downstream paths.