Rheo documentation
Developer Guide

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

  1. Install the host packages in your app:

    pnpm add react-native-purchases react-native-purchases-ui
  2. Configure RevenueCat at app startup with your own SDK key. Rheo never configures Purchases for you:

    import Purchases from 'react-native-purchases';
    
    Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_RC_KEY! });
  3. Define an offering (and optionally a placement) in the RevenueCat dashboard. Copy the identifier.

  4. 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.

  5. Add a RevenueCat paywall node to a flow from the canvas add menu. Connect each outcome handle (or rely on Fallback).

  6. Publish the flow and run it through the SDK — the host triggers <Flow> or useFlow exactly 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:

OutcomeWhen it fires
purchase_completedThe user completed a purchase from the paywall.
restore_completedA restore unlocked a new entitlement. Restores with no entitlement land on dismissed / failed.
dismissedThe user closed the paywall, or the modal was not presented (e.g. presentPaywallIfNeeded opted out).
failedRevenueCat 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.

KeySet whenNotes
onb_rc_last_eventAlwayspurchase_completed / restore_completed / dismissed / failed.
onb_rc_last_offering_idThe node configured an offering idCopied from the manifest node config.
onb_rc_last_product_idA successful purchase exposed a product idRead from RevenueCat customer info after purchase_completed.
onb_rc_last_period_typeRevenueCat supplied a period typenormal / 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_free

Expo 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) so react-native-purchases is linked.
  • Expo Go users see the paywall step resolve as failed (the lazy require returns null); 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 Purchases for you. If react-native-purchases-ui is missing at runtime, the paywall step resolves as failed and 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 sdkAttributes on <RheoProvider>) if you need that.

Analytics

Three analytics events are emitted automatically:

  • surface_presented — when the paywall is shown
  • surface_outcome — on every resolve, including failed
  • iap_purchase — only on purchase_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-ui is installed in your host app and that Purchases.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_completed only when the restore unlocks a new entitlement; otherwise the outcome is dismissed (or failed on error). Map both branches explicitly if you need different downstream paths.