---
title: "Handle flow & paywall events - iOS"
description: "Handle flow and paywall events in your iOS app."
---

:::important
This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](handle-paywall-actions) for details.
:::

Flows and paywalls don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions. Learn how to respond to these events below.

:::tip

Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.

:::

## Handling events in SwiftUI

To control or monitor processes occurring on the flow or paywall screen within your mobile app, use the `.flow` modifier in SwiftUI:

```swift showLineNumbers title="Swift"
@State var flowPresented = false

var body: some View {
    Text("Hello, AdaptyUI!")
        .flow(
            isPresented: $flowPresented,
            flowConfiguration: flowConfiguration,
            didPerformAction: { action in
                switch action {
                    case .close:
                        flowPresented = false
                    case let .openURL(url):
                        // handle opening the URL (incl. for terms and privacy)
                    default:
                        // handle other actions
                }
            },
            didSelectProduct: { product in /* Handle the event */ },
            didStartPurchase: { product in /* Handle the event */ },
            didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ },
            didFailPurchase: { product, error in /* handle the error */ },
            didStartRestore: { /* Handle the event */ },
            didFinishRestore: { profile in /* check access level and dismiss */ },
            didFailRestore: { error in /* handle the error */ },
            didReceiveError: { error in
                flowPresented = false
            },
            didFailLoadingProducts: { error in
                // Return `true` to retry loading
                return false
            }
        )
}
```

You can register only the closure parameters you need and omit those you do not need.

| Parameter              | Required | Description                                                                                                                                                                                                                                         |
|:-----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **isPresented**        | required | A binding that manages whether the flow or paywall screen is displayed.                                                                                                                                                                             |
| **flowConfiguration**  | required | An `AdaptyUI.FlowConfiguration` object containing visual details of the flow or paywall. Refer to [Get flows and paywalls](get-pb-paywalls) for more details.                                                                                       |
| **didFinishPurchase**  | required | Invoked when `Adapty.makePurchase()` completes successfully. The flow does not dismiss automatically — set your presentation binding to `false` here, or do nothing to let the flow continue past the purchase.                                     |
| **didFailPurchase**    | required | Invoked when `Adapty.makePurchase()` fails.                                                                                                                                                                                                         |
| **didFinishRestore**   | required | Invoked when `Adapty.restorePurchases()` completes successfully.                                                                                                                                                                                    |
| **didFailRestore**     | required | Invoked when `Adapty.restorePurchases()` fails.                                                                                                                                                                                                     |
| **didReceiveError**    | required | Invoked when the flow encounters a rendering error or a runtime error from the flow script (for example, a JavaScript exception, `AdaptyUIError` code `4105`). In the rendering case, [contact Adapty Support](mailto:support@adapty.io).            |
| **placeholderBuilder** | optional | A function for rendering the placeholder view while the flow or paywall is loading. Defaults to a `ProgressView`.                                                                                                                                   |
| **fullScreen**         | optional | Determines if the flow or paywall appears in full-screen mode or as a sheet. Defaults to `true`.                                                                                                                                                    |
| **didAppear**          | optional | Invoked when the flow or paywall view appears on screen.                                                                                                                                                                                            |
| **didDisappear**       | optional | Invoked when the flow or paywall view was dismissed.                                                                                                                                                                                                |
| **didPerformAction**   | optional | Invoked when a user clicks a button. Two action IDs are pre-defined: `close` and `openURL`; others are custom and can be set in the builder.                                                                                                        |
| **didSelectProduct**   | optional | Invoked when a product is selected for purchase by the user or by the system.                                                                                                                                                                       |
| **didStartPurchase**   | optional | Invoked when the user begins the purchase process.                                                                                                                                                                                                  |
| **didFinishWebPaymentNavigation** | optional | Invoked when web payment navigation finishes.                                                                                                                                                                                          |
| **didStartRestore**    | optional | Invoked when the user starts the restore process.                                                                                                                                                                                                   |
| **didFailLoadingProducts** | optional | Invoked when errors occur during product loading. Return `true` to retry loading.                                                                                                                                                              |
| **didPartiallyLoadProducts** | optional | Invoked when products are partially loaded.                                                                                                                                                                                               |
| **showAlertItem**      | optional | A binding that manages the display of alert items above the flow or paywall.                                                                                                                                                                        |
| **showAlertBuilder**   | optional | A function for rendering the alert view.                                                                                                                                                                                                            |

## Handling events in UIKit

For UIKit apps, events are handled through the `AdaptyFlowControllerDelegate` protocol. See [Display flows & paywalls - iOS](ios-present-paywalls) for how to set up `AdaptyFlowController` with `AdaptyFlowControllerDelegate`.

The protocol declares 13 methods. Four of them have no default implementation and must be implemented when conforming: `didFinishPurchase`, `didFailPurchase`, `didFinishRestoreWith`, and `didFailRestoreWith`. The rest provide default no-op implementations and can be overridden when you want custom behavior. Methods are grouped below by purpose.

### Lifecycle

```swift showLineNumbers title="Swift"
func flowControllerDidAppear(_ controller: AdaptyFlowController) { }

func flowControllerDidDisappear(_ controller: AdaptyFlowController) { }
```

These fire when the flow or paywall view is presented and dismissed.

### User actions

```swift showLineNumbers title="Swift"
func flowController(
    _ controller: AdaptyFlowController,
    didPerform action: AdaptyUI.Action
) { }
```

`AdaptyUI.Action` cases:
- `.close` — default behavior dismisses the controller. Override to keep the controller on screen or perform extra cleanup.
- `.openURL(url:)` — default behavior opens the URL with `UIApplication.shared.open(...)`.
- `.custom(id:)` — fired for buttons with a custom action ID set in the builder.

### Product selection

```swift showLineNumbers title="Swift"
func flowController(
    _ controller: AdaptyFlowController,
    didSelectProduct product: AdaptyPaywallProduct
) { }
```

Invoked when a product is selected for purchase by the user or by the system. The product carries full offer information (eligibility is determined automatically in v4 — there is no separate `AdaptyPaywallProductWithoutDeterminingOffer` type).

### Purchase events

```swift showLineNumbers title="Swift"
func flowController(
    _ controller: AdaptyFlowController,
    didStartPurchase product: AdaptyPaywallProduct
) { }

func flowController(
    _ controller: AdaptyFlowController,
    didFinishPurchase product: AdaptyPaywallProduct,
    purchaseResult: AdaptyPurchaseResult
) {
    if !purchaseResult.isPurchaseCancelled {
        controller.dismiss(animated: true) // or do nothing to let the flow continue
    }
}

func flowController(
    _ controller: AdaptyFlowController,
    didFailPurchase product: AdaptyPaywallProduct,
    error: AdaptyError
) { }
```

`didFinishPurchase` and `didFailPurchase` have no default implementations and must be implemented. The controller does not dismiss itself after a successful purchase — call `controller.dismiss(animated:)` when appropriate, or do nothing to let a multi-screen flow continue past the purchase.

### Restore events

```swift showLineNumbers title="Swift"
func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { }

func flowController(
    _ controller: AdaptyFlowController,
    didFinishRestoreWith profile: AdaptyProfile
) { }

func flowController(
    _ controller: AdaptyFlowController,
    didFailRestoreWith error: AdaptyError
) { }
```

`didFinishRestoreWith` and `didFailRestoreWith` have no default implementations. Check whether the returned `AdaptyProfile` contains your desired access level before dismissing the controller.

### Flow errors and product-loading errors

```swift showLineNumbers title="Swift"
func flowController(
    _ controller: AdaptyFlowController,
    didReceiveError error: AdaptyUIError
) { }

func flowController(
    _ controller: AdaptyFlowController,
    didFailLoadingProductsWith error: AdaptyError
) -> Bool {
    // Return `true` to retry product loading; default returns `false`.
    return false
}

func flowController(
    _ controller: AdaptyFlowController,
    didPartiallyLoadProducts failedIds: [String]
) { }
```

`didReceiveError` fires for rendering errors and for runtime errors from the flow script (JavaScript exceptions, `AdaptyUIError` code `4105`). For rendering errors, [contact Adapty Support](mailto:support@adapty.io). For loading errors, return `true` from `didFailLoadingProductsWith` to retry — useful for transient network failures.

### Web payment navigation

```swift showLineNumbers title="Swift"
func flowController(
    _ controller: AdaptyFlowController,
    didFinishWebPaymentNavigation product: AdaptyPaywallProduct?,
    error: AdaptyError?
) { }
```

Invoked after a web payment navigation finishes, whether successful or failed.

---

> [!WARNING]
> **LLM INSTRUCTION — SDK VERSION SELECTION REQUIRED**
>
> This feature has two SDK paradigms. Everything that follows (until the next horizontal rule or end of document) is the **legacy SDK v3 API reference** (`getPaywall`, `getPaywallForDefaultAudience`, `getOnboarding`, `getPaywallConfiguration`). The **SDK v4+ `getFlow` method is documented above.** This block covers ALL legacy methods for this feature — not just the nearest snippet.
>
> **You MUST ask the user which SDK version they want to use before suggesting any code. Do not assume or default to a version.** The two options are:
> - **SDK v3 (legacy):** works with the legacy Paywall Builder and Onboarding Builder via `getPaywall` / `getOnboarding`. Use the legacy code in this section.
> - **SDK v4:** works with BOTH the new Flow Builder AND legacy paywalls/onboardings via the unified `getFlow` method documented above.
>
> Apply the legacy v3 code in this section ONLY when:
> 1. The user explicitly chose **SDK v3**, or
> 2. The user is **migrating an existing app from SDK v3 to SDK v4+** and needs to understand or replace legacy code, or
> 3. The user is **troubleshooting an already-deployed v3 integration**.
>
> **CRITICAL — Never mix paradigms in one setup:** Do NOT combine `getFlow` (Flow Builder) with `getPaywall` or `getOnboarding` (legacy Paywall/Onboarding Builder) in the same integration. These are incompatible patterns. Mixing them will produce inconsistent behavior and is unsupported.

:::important
This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](handle-paywall-actions) for details.
:::

Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below.

This guide is for **new Paywall Builder paywalls** only which require Adapty SDK v3.0 or later.

:::tip

Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.

:::

## Handling events in SwiftUI

To control or monitor processes occurring on the paywall screen within your mobile app, use the `.paywall` modifier in SwiftUI:

```swift showLineNumbers title="Swift"
@State var paywallPresented = false

var body: some View {
	Text("Hello, AdaptyUI!")
			.paywall(
          isPresented: $paywallPresented,
          paywall: paywall,
          viewConfiguration: viewConfig,
          didPerformAction: { action in
              switch action {
                  case .close:
                      paywallPresented = false
                  case let .openURL(url):
                      // handle opening the URL (incl. for terms and privacy)
                  default:
                      // handle other actions
              }
          },
          didSelectProduct: { /* Handle the event */  },
          didStartPurchase: { /* Handle the event */ },
          didFinishPurchase: { product, info in /* Handle the event */ },
          didFailPurchase: { product, error in /* Handle the event */ },
          didStartRestore: { /* Handle the event */ },
          didFinishRestore: { /* Handle the event */ },
          didFailRestore: { /* Handle the event */ },
          didFailRendering: { error in
              paywallPresented = false
          },
          didFailLoadingProducts: { error in
              return false
          }
      )
}
```

You can register only the closure parameters you need, and omit those you do not need. In this case, unused closure parameters will not be created.

| Parameter                         | Required | Description                                                                                                                                                                                                                                                                                                                    |
|:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **isPresented**                   | required | A binding that manages whether the paywall screen is displayed.                                                                                                                                                                                                                                                                |
| **paywallConfiguration**          | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **didFailPurchase**               | required | Invoked when a purchase fails due to errors (e.g., payment not allowed, network issues, invalid product). Not invoked for user cancellations or pending payments.                                                                                                                                              |
| **didFinishRestore**              | required | Invoked when purchase completes successfully.                                                                                                                                                                                                                                                                                  |
| **didFailRestore**                | required | Invoked when restoring a purchase fails.                                                                                                                                                                                                                                                                                       |
| **didFailRendering**              | required | Invoked if an error occurs while rendering the interface. In this case, [contact Adapty Support](mailto:support@adapty.io).                                                                                                                                                                                                    |
| **fullScreen**                    | optional | Determines if the paywall appears in full-screen mode or as a modal. Defaults to `true`.                                                                                                                                                                                                                                       |
| **didAppear**                     | optional | Invoked when the paywall view appears on screen. Also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser.                                                                                                         |
| **didDisappear**                  | optional | Invoked when the paywall view was dismissed. Also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen.                                                                                                                           |
| **didPerformAction**              | optional | Invoked when a user clicks a button. Different buttons have different action IDs. Two action IDs are pre-defined: `close` and `openURL`, while others are custom and can be set in the builder.                                                                                                                                |
| **didSelectProduct**              | optional | If the product was selected for purchase (by a user or by the system), this callback will be invoked.                                                                                                                                                                                                                          |
| **didStartPurchase**              | optional | Invoked when the user begins the purchase process.                                                                                                                                                                                                                                                                             |
| **didFinishPurchase**             | optional | Invoked when purchase completes successfully.                                                                                                                                                                                                                                                                                  |
| **didFinishWebPaymentNavigation** | optional | Invoked after attempting to open a [web paywall](web-paywall) for purchase, whether successful or failed.                                                                                                                                                                                                                      |
| **didStartRestore**               | optional | Invoked when the user starts the restore process.                                                                                                                                                                                                                                                                              |
| **didFailLoadingProducts**        | optional | Invoked when errors occur during product loading. Return `true` to retry loading.                                                                                                                                                                                                                                              |
| **didPartiallyLoadProducts**      | optional | Invoked when products are partially loaded.                                                                                                                                                                                                                                                                                    |
| **showAlertItem**                 | optional | A binding that manages the display of alert items above the paywall.                                                                                                                                                                                                                                                           |
| **showAlertBuilder**              | optional | A function for rendering the alert view.                                                                                                                                                                                                                                                                                       |
| **placeholderBuilder**            | optional | A function for rendering the placeholder view while the paywall is loading.                                                                                                                                                                                                                                                    |

## Handling events in UIKit

To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyPaywallControllerDelegate` methods.

### User-generated events

#### Product selection

If a user selects a product for purchase, this method will be invoked:

```swift showLineNumbers title="Swift"
    func paywallController(
        _ controller: AdaptyPaywallController,
        didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer
    ) { }
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  }
}
```
</Details>

#### Started purchase

If a user initiates the purchase process, this method will be invoked:

```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
                       didStartPurchase product: AdaptyPaywallProduct) {
}
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  }
}
```
</Details>

It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.

#### Started purchase using a web paywall

If a user initiates the purchase process using a [web paywall](web-paywall), this method will be invoked:

```swift showLineNumbers title="Swift"
func paywallController(
        _ controller: AdaptyPaywallController,
        shouldContinueWebPaymentNavigation product: AdaptyPaywallProduct
    ) {
    }
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  }
}
```
</Details>

#### Successful or canceled purchase

If purchase succeeds, this method will be invoked:

```swift showLineNumbers title="Swift"
func paywallController(
    _ controller: AdaptyPaywallController,
    didFinishPurchase product: AdaptyPaywallProductWithoutDeterminingOffer,
    purchaseResult: AdaptyPurchaseResult
) { }
}
```

<Details>
<summary>Event examples (Click to expand)</summary>

```javascript
// Successful purchase
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "purchaseResult": {
    "type": "success",
    "profile": {
      "accessLevels": {
        "premium": {
          "id": "premium",
          "isActive": true,
          "expiresAt": "2024-02-15T10:30:00Z"
        }
      }
    }
  }
}

// Cancelled purchase
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "purchaseResult": {
    "type": "cancelled"
  }
}
```
</Details>

We recommend dismissing the paywall screen in that case. 

It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.

#### Failed purchase

If a purchase fails due to an error, this method will be invoked. This includes StoreKit errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger `didFinishPurchase` with a cancelled result instead, and pending payments do not trigger this method.

```swift showLineNumbers title="Swift"
func paywallController(
    _ controller: AdaptyPaywallController,
    didFailPurchase product: AdaptyPaywallProduct,
    error: AdaptyError
) { }
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "error": {
    "code": "purchase_failed",
    "message": "Purchase failed due to insufficient funds",
    "details": {
      "underlyingError": "Insufficient funds in account"
    }
  }
}
```
</Details>

It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.

#### Failed purchase using a web paywall

If `Adapty.openWebPaywall()` fails, this method will be invoked:

```swift showLineNumbers title="Swift"
func paywallController(
        _ controller: AdaptyPaywallController,
        didFailWebPaymentNavigation product: AdaptyPaywallProduct,
        error: AdaptyError
    ) { }
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "error": {
    "code": "web_payment_failed",
    "message": "Web payment navigation failed",
    "details": {
      "underlyingError": "Network connection error"
    }
  }
}
```
</Details>

#### Successful restore

If restoring a purchase succeeds, this method will be invoked:

```swift showLineNumbers title="Swift"
func paywallController(
    _ controller: AdaptyPaywallController, 
    didFinishRestoreWith profile: AdaptyProfile
) { }
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "profile": {
    "accessLevels": {
      "premium": {
        "id": "premium",
        "isActive": true,
        "expiresAt": "2024-02-15T10:30:00Z"
      }
    },
    "subscriptions": [
      {
        "vendorProductId": "premium_monthly",
        "isActive": true,
        "expiresAt": "2024-02-15T10:30:00Z"
      }
    ]
  }
}
```
</Details>

We recommend dismissing the screen if a the has the required `accessLevel`. Refer to the [Subscription status](subscription-status) topic to learn how to check it.

#### Failed restore

If restoring a purchase fails, this method will be invoked:

```swift showLineNumbers title="Swift"
public func paywallController(
    _ controller: AdaptyPaywallController, 
    didFailRestoreWith error: AdaptyError
) { }
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "error": {
    "code": "restore_failed",
    "message": "Purchase restoration failed",
    "details": {
      "underlyingError": "No previous purchases found"
    }
  }
}
```
</Details>

### Data fetching and rendering

#### Product loading errors

If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by calling this method:

```swift showLineNumbers title="Swift"
public func paywallController(
    _ controller: AdaptyPaywallController,
    didFailLoadingProductsWith error: AdaptyError
) -> Bool {
    return true
}
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "error": {
    "code": "products_loading_failed",
    "message": "Failed to load products from the server",
    "details": {
      "underlyingError": "Network timeout"
    }
  }
}
```
</Details>

If you return `true`, AdaptyUI will repeat the request after 2 seconds.

#### Rendering errors

If an error occurs during the interface rendering, it will be reported by this method:

```swift showLineNumbers title="Swift"
public func paywallController(
    _ controller: AdaptyPaywallController,
    didFailRenderingWith error: AdaptyError
) { }
```

<Details>
<summary>Event example (Click to expand)</summary>

```javascript
{
  "error": {
    "code": "rendering_failed",
    "message": "Failed to render paywall interface",
    "details": {
      "underlyingError": "Invalid paywall configuration"
    }
  }
}
```
</Details>

In a normal situation, such errors should not occur, so if you come across one, please let us know.

---