Skip to main content
On iOS you own RevenueCat, and FlowPilot reads through it. You configure RevenueCat as you normally would, hand FlowPilot a one-line adapter, then present a paywall placement exactly like any other flow. This page assumes you already have the FlowPilot iOS SDK installed and configured.
Before you startPaywalls build on the standard FlowPilot setup, plus RevenueCat:
  • A FlowPilot workspace, an app inside it, and an SDK API key for that app.
  • A RevenueCat account with your products, entitlements, and an offering configured for your app.
  • Paywalls enabled for your workspace. If you do not see a Paywalls item in the dashboard sidebar, reach out and we will turn it on.

Add the RevenueCat adapter

The RevenueCat adapter ships as a separate Swift package, FlowPilotRevenueCat, on purpose. Apps that use FlowPilot but not RevenueCat never resolve or fetch purchases-ios. Add this package (and RevenueCat) only in an app that shows paywalls.
TODO: confirm the published FlowPilotRevenueCat package URL and version rule, then replace the placeholders below.
dependencies: [
    .package(url: "https://github.com/flowpilothq/flowpilot-ios-sdk.git", from: "1.4.1"),
    .package(url: "https://github.com/RevenueCat/purchases-ios.git", from: "5.0.0"),
    // The RevenueCat adapter package (FlowPilotRevenueCat).
],
targets: [
    .target(
        name: "MyApp",
        dependencies: [
            .product(name: "FlowPilotSDK", package: "flowpilot-ios-sdk"),
            .product(name: "FlowPilotRevenueCat", package: "FlowPilotRevenueCat"),
            .product(name: "RevenueCat", package: "purchases-ios"),
        ]
    )
]
Paywall support is in FlowPilot iOS SDK 1.4.1 and later.

Wire it up

The order matters: configure RevenueCat first, then FlowPilot, then register the adapter before you present a paywall.
1

Configure RevenueCat (host-owned)

Configure RevenueCat yourself, with your own API key and your own user identity. FlowPilot never calls Purchases.configure, never signs users in or out, and never claims the RevenueCat delegate.
import RevenueCat

Purchases.configure(withAPIKey: "appl_XXXXXXXXXXXX")
2

Configure FlowPilot

import FlowPilotSDK

FlowPilot.configure(
    FlowPilotConfiguration(
        apiKey: "fp_live_xxxxxxxxxxxxxxxx",
        appId: "your-app-id"
    )
)
3

Register the adapter

Hand FlowPilot the RevenueCat adapter. This is the only monetization-specific setup step. Do it once, after both are configured.
import FlowPilotRevenueCat

FlowPilot.shared?.registerPurchaseProvider(FlowPilotRevenueCatProvider())

Present a paywall

A paywall is a placement, so you present it with presentPlacement, the same call you use for onboarding. What changes is the result: a paywall flow reports a paywallOutcome.
let result = try await FlowPilot.shared?.presentPlacement(
    "post_onboarding_paywall",
    from: viewController
)

switch result?.paywallOutcome {
case .purchased, .restored, .skippedEntitled:
    unlockFeature()
case .dismissed where result?.paywallGatingMode == .soft:
    unlockFeature()          // soft paywall: dismissal continues
default:
    unlockFeature()          // fail-open on every skip or failure
}

The paywall outcome

FlowResult.paywallOutcome is nil for non-paywall flows. For a paywall it is one of:
OutcomeMeaning
.purchasedThe user bought a subscription and the entitlement is active.
.restoredA restore found an active entitlement.
.dismissedThe user closed the paywall without buying. Read paywallGatingMode to decide what that means.
.skippedEntitledThe user already held the entitlement, so the paywall was not shown.
.skippedNoFlowNothing was configured to present (paused placement, no flow).
.skippedAudienceThe user did not match the audience.
.skippedHoldoutThe user was bucketed into a holdout that shows no paywall.
.failedSomething went wrong. Fail-open: let your feature continue.
The base FlowOutcome (.completed, .dismissed, .error) is unchanged, so your existing result handling still compiles. See Results and outcomes.
FlowPilot reports the outcome and the gating mode. It does not enforce feature access. Your app decides what a .dismissed result on a hard paywall means. A user in a billing grace period or retry state is reported as entitled by RevenueCat, so skipIfEntitled naturally leaves them alone.

Fail-open by default

If no paywall is configured, the audience does not match, the user is in a holdout, or the flow fails to load, FlowPilot does not present anything and your app’s own path continues. This is the safe default: a paywall problem never blocks your app. When a feature must stay locked behind a paywall, your app enforces that using the outcome above.

Check ahead of time with a peek

To render a lock badge or decide whether to gate a feature before presenting, use peekPlacement. It resolves whether a paywall would show for this placement and this user, without emitting an impression or locking the user into an experiment variant.
let peek = await FlowPilot.shared?.peekPlacement("premium_export")

if peek?.willPresentPaywall == true {
    showLockBadge()
}
PaywallPeekResult reports availability (available, skippedEntitled, notPaywall, noFlow, or unknown), plus isEntitled, gatingMode, and requiredEntitlement. On an inconclusive peek (offline or an error) the availability is unknown, so treat it as “do not gate” rather than showing a broken lock.

Test before you ship

Real purchases need a device and a signed build. In development, the fastest end-to-end path is the RevenueCat Test Store, which exercises the full purchase flow without App Store Connect. Use an Apple sandbox account for accelerated-renewal and restore testing, and always run one pass on a real device before submitting.

Diagnostics

The SDK logs each purchase state transition. Set the log level to see them, and turn off diagnostic reporting if you prefer:
FlowPilotConfiguration(
    apiKey: "fp_live_xxxxxxxxxxxxxxxx",
    appId: "your-app-id",
    logLevel: .debug,             // see purchase and offering breadcrumbs
    disableErrorReporting: false  // set true to opt out of scoped error reports
)
Diagnostic reports carry error codes and FlowPilot identifiers only. They never include RevenueCat keys, prices, or user data.

Common mistakes

  • Registering the provider before configuring RevenueCat. Configure RevenueCat first. The adapter reads Purchases.shared, which must already be configured.
  • Letting FlowPilot own identity. Keep Purchases.configure, log in, and log out in your app. FlowPilot only reads through the adapter.
  • Treating a skip as an error. Every skipped* outcome is a normal, fail-open result. Continue your app.
  • Testing purchases in the simulator. Use the RevenueCat Test Store or a sandbox account on a real device.