> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS integration

> Configure RevenueCat in your app, register the FlowPilot adapter, and present a paywall placement.

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](/ios-sdk/quickstart).

<Info>
  **Before you start**

  Paywalls build on the standard FlowPilot setup, plus RevenueCat:

  * A FlowPilot [workspace](/dashboard/workspaces-and-apps), an [app](/dashboard/workspaces-and-apps) inside it, and an [SDK API key](/dashboard/api-keys) for that app.
  * A [RevenueCat](https://www.revenuecat.com) 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.
</Info>

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

<Note>TODO: confirm the published `FlowPilotRevenueCat` package URL and version rule, then replace the placeholders below.</Note>

```swift theme={null}
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.

<Steps>
  <Step title="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.

    ```swift theme={null}
    import RevenueCat

    Purchases.configure(withAPIKey: "appl_XXXXXXXXXXXX")
    ```
  </Step>

  <Step title="Configure FlowPilot">
    ```swift theme={null}
    import FlowPilotSDK

    FlowPilot.configure(
        FlowPilotConfiguration(
            apiKey: "fp_live_xxxxxxxxxxxxxxxx",
            appId: "your-app-id"
        )
    )
    ```
  </Step>

  <Step title="Register the adapter">
    Hand FlowPilot the RevenueCat adapter. This is the only monetization-specific setup step. Do it once, after both are configured.

    ```swift theme={null}
    import FlowPilotRevenueCat

    FlowPilot.shared?.registerPurchaseProvider(FlowPilotRevenueCatProvider())
    ```
  </Step>
</Steps>

## 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`.

```swift theme={null}
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:

| Outcome            | Meaning                                                                                         |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| `.purchased`       | The user bought a subscription and the entitlement is active.                                   |
| `.restored`        | A restore found an active entitlement.                                                          |
| `.dismissed`       | The user closed the paywall without buying. Read `paywallGatingMode` to decide what that means. |
| `.skippedEntitled` | The user already held the entitlement, so the paywall was not shown.                            |
| `.skippedNoFlow`   | Nothing was configured to present (paused placement, no flow).                                  |
| `.skippedAudience` | The user did not match the audience.                                                            |
| `.skippedHoldout`  | The user was bucketed into a holdout that shows no paywall.                                     |
| `.failed`          | Something 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](/ios-sdk/results-and-outcomes).

<Note>
  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.
</Note>

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

```swift theme={null}
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:

```swift theme={null}
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.

## Related pages

* [Paywalls overview](/paywalls/index)
* [Build a paywall](/paywalls/building-a-paywall)
* [Expo integration](/paywalls/expo)
* [Results and outcomes](/ios-sdk/results-and-outcomes)
* [App Store review](/paywalls/app-review)
