---
title: "Flutter - Handle flow & paywall events"
description: "Discover how to handle subscription-related events in Flutter using Adapty to track user interactions effectively."
---

:::important
This guide covers event handling for purchases, restorations, product selection, and rendering. Closing the view and opening links are handled by the default `flowViewDidPerformAction` implementation — see our [guide on handling button actions](flutter-handle-paywall-actions) to override them or to handle custom button actions.
:::

Flows and paywalls configured with the 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 flow or paywall. Learn how to respond to these events below.

To control or monitor processes occurring on the flow or paywall screen within your mobile app, implement the `AdaptyUIFlowsEventsObserver` methods and set the observer before presenting any screen:

```dart showLineNumbers title="Flutter"
AdaptyUI().setFlowsEventsObserver(this);
```

Three observer methods are **required** — your class won't compile without them: `flowViewDidFinishPurchase`, `flowViewDidFinishRestore`, and `flowViewDidReceiveError`. All other methods are optional. To detach a previously set observer, pass `null` to `setFlowsEventsObserver`.

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

:::

The event examples below show the properties available on each object, with illustrative values in comments.

### User-generated events

#### View appeared

This method is invoked when the flow or paywall view is presented on the screen.

:::note
On iOS, 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.
:::

```dart showLineNumbers title="Flutter"
void flowViewDidAppear(AdaptyUIFlowView view) {
}
```

#### View disappeared

This method is invoked when the flow or paywall view is dismissed from the screen.

:::note
On iOS, 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.
:::

```dart showLineNumbers title="Flutter"
void flowViewDidDisappear(AdaptyUIFlowView view) {
}
```

#### Product selection

If a product is selected for purchase (by a user or by the system), this method will be invoked:

```dart showLineNumbers title="Flutter"
void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) {
}
```

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

```dart
void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) {
  // productId is a String:
  productId; // 'premium_monthly'
}
```
</Details>

#### Started purchase

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

```dart showLineNumbers title="Flutter"
void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) {
}
```

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

```dart
void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) {
  // product — AdaptyPaywallProduct:
  product.vendorProductId;        // 'premium_monthly'
  product.localizedTitle;         // 'Premium Monthly'
  product.localizedDescription;   // 'Premium subscription for 1 month'
  product.price.amount;           // 9.99            (double)
  product.price.currencyCode;     // 'USD'
  product.price.localizedString;  // '$9.99'
}
```
</Details>

#### Finished purchase

This method is **required**. It is invoked when a purchase succeeds, the user cancels their purchase, or the purchase appears to be pending:

```dart showLineNumbers title="Flutter"
void flowViewDidFinishPurchase(AdaptyUIFlowView view, 
                               AdaptyPaywallProduct product, 
                               AdaptyPurchaseResult purchaseResult) {
    switch (purchaseResult) {
      case AdaptyPurchaseResultSuccess(profile: final profile):
        // successful purchase
        break;
      case AdaptyPurchaseResultPending():
        // purchase is pending
        break;
      case AdaptyPurchaseResultUserCancelled():
        // user cancelled the purchase
        break;
      default:
        break;
    }
}
```

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

```dart
void flowViewDidFinishPurchase(AdaptyUIFlowView view,
                               AdaptyPaywallProduct product,
                               AdaptyPurchaseResult purchaseResult) {
  // product — AdaptyPaywallProduct:
  product.vendorProductId; // 'premium_monthly'

  switch (purchaseResult) {
    case AdaptyPurchaseResultSuccess(profile: final profile):
      // profile — AdaptyProfile:
      profile.accessLevels['premium']?.isActive;  // true
      profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30)
      break;
    case AdaptyPurchaseResultPending():
      // no additional data
      break;
    case AdaptyPurchaseResultUserCancelled():
      // no additional data
      break;
  }
}
```
</Details>

:::info
Unlike v3, this method has no default behavior — the view is no longer dismissed automatically after a successful purchase. Decide what happens next yourself: continue the flow or call `view.dismiss()`. Refer to [Respond to button actions](flutter-handle-paywall-actions) for details on dismissing a screen.
:::

#### Finished web payment navigation

This method is invoked after an attempt to open a [web paywall](web-paywall) for a specific product. This includes both successful and failed navigation attempts:

```dart showLineNumbers title="Flutter"
void flowViewDidFinishWebPaymentNavigation(AdaptyUIFlowView view, 
                                           AdaptyPaywallProduct? product, 
                                           AdaptyError? error) {
}
```

**Parameters:**

| Parameter   | Description                                                                                        |
|:------------|:---------------------------------------------------------------------------------------------------|
| **product** | An `AdaptyPaywallProduct` for which the web paywall was opened. Can be `null`.                     |
| **error**   | An `AdaptyError` object if the web paywall navigation failed; `null` if navigation was successful. |

#### Failed purchase

This method is invoked when a purchase fails (for example, due to payment issues or network errors). It does **not** fire for user-initiated cancellations or pending transactions—those are handled by `flowViewDidFinishPurchase`:

```dart showLineNumbers title="Flutter"
void flowViewDidFailPurchase(AdaptyUIFlowView view, 
                             AdaptyPaywallProduct product, 
                             AdaptyError error) {
}
```

#### Started restore

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

```dart showLineNumbers title="Flutter"
void flowViewDidStartRestore(AdaptyUIFlowView view) {
}
```

#### Successful restore

This method is **required**. If restoring a purchase succeeds, it will be invoked:

```dart showLineNumbers title="Flutter"
void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) {
}
```

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

```dart
void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) {
  // profile — AdaptyProfile:
  profile.accessLevels['premium']?.isActive;            // true
  profile.accessLevels['premium']?.expiresAt;           // DateTime(2027, 2, 15, 10, 30)
  profile.subscriptions['premium_monthly']?.isActive;   // true
  profile.subscriptions['premium_monthly']?.expiresAt;  // DateTime(2027, 2, 15, 10, 30)
}
```
</Details>

We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](flutter-listen-subscription-changes) topic to learn how to check it and to [Respond to button actions](flutter-handle-paywall-actions) topic to learn how to dismiss a screen.

#### Failed restore

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

```dart showLineNumbers title="Flutter"
void flowViewDidFailRestore(AdaptyUIFlowView view, AdaptyError error) {
}
```

### 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 invoking this method:

```dart showLineNumbers title="Flutter"
void flowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError error) {
}
```

#### View errors

This method is **required**. It replaces the v3 `paywallViewDidFailRendering` method: errors that occur during the interface rendering, as well as other view errors, are reported by calling it. Once you implement it, dismissal is up to you — we recommend dismissing the view on such errors, which is also what the SDK's built-in default does when no observer is set:

```dart showLineNumbers title="Flutter"
void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) {
  // log the error and dismiss the broken view
  view.dismiss();
}
```

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

### Analytics events

The optional `flowViewDidReceiveAnalyticEvent` method is reserved for custom analytic events from a flow. Flows don't emit these to your code yet, so you don't need to implement it.

### Handle purchases in observer mode

If you activated the SDK in [Observer mode](implement-observer-mode-flutter) and present an Adapty-rendered flow or paywall, the SDK does not make purchases for you. When a user taps the purchase or restore button, the SDK calls your `AdaptyUIObserverModeResolver` instead. See [Present flows in Observer mode](flutter-present-flows-in-observer-mode) for the full setup.

### Handle system requests

The `AdaptyUISystemRequestsHandler` (registered via `AdaptyUI().setSystemRequestsHandler(...)`) is reserved for system requests from a flow: OS permission prompts (such as push notifications or camera access) and App Store review requests. Flows don't trigger these requests yet, so you don't need to register a handler.

If you do register one, note that `handlePermission` is the class's required method — request the permission with your own code, then return `AdaptyUIPermissionResult.granted()` or `AdaptyUIPermissionResult.denied()`; `handleAppReviewRequest` is optional.

---

> [!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](flutter-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.

:::warning

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

:::

To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyUIPaywallsEventsObserver` methods and set the observer before presenting any screen:

```dart showLineNumbers title="Flutter"
AdaptyUI().setPaywallsEventsObserver(this);
```

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

:::

The event examples below show the properties available on each object, with illustrative values in comments.

### User-generated events

#### Paywall appeared

This method is invoked when the paywall view is presented on the screen.

:::note
On iOS, 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.
:::

```dart showLineNumbers title="Flutter"
void paywallViewDidAppear(AdaptyUIPaywallView view) {
}
```

#### Paywall disappeared

This method is invoked when the paywall view is dismissed from the screen.

:::note
On iOS, 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.
:::

```dart showLineNumbers title="Flutter"
void paywallViewDidDisappear(AdaptyUIPaywallView view) {
}
```

#### Product selection

If a product is selected for purchase (by a user or by the system), this method will be invoked:

```dart showLineNumbers title="Flutter"
void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) {
}
```

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

```dart
void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) {
  // productId is a String:
  productId; // 'premium_monthly'
}
```
</Details>

#### Started purchase

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

```dart showLineNumbers title="Flutter"
void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) {
}
```

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

```dart
void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) {
  // product — AdaptyPaywallProduct:
  product.vendorProductId;        // 'premium_monthly'
  product.localizedTitle;         // 'Premium Monthly'
  product.localizedDescription;   // 'Premium subscription for 1 month'
  product.price.amount;           // 9.99            (double)
  product.price.currencyCode;     // 'USD'
  product.price.localizedString;  // '$9.99'
}
```
</Details>

#### Finished purchase

This method is invoked when a purchase succeeds, the user cancels their purchase, or the purchase appears to be pending:

```dart showLineNumbers title="Flutter"
void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, 
                                  AdaptyPaywallProduct product, 
                                  AdaptyPurchaseResult purchaseResult) {
    switch (purchaseResult) {
      case AdaptyPurchaseResultSuccess(profile: final profile):
        // successful purchase
        break;
      case AdaptyPurchaseResultPending():
        // purchase is pending
        break;
      case AdaptyPurchaseResultUserCancelled():
        // user cancelled the purchase
        break;
      default:
        break;
    }
}
```

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

```dart
void paywallViewDidFinishPurchase(AdaptyUIPaywallView view,
                                  AdaptyPaywallProduct product,
                                  AdaptyPurchaseResult purchaseResult) {
  // product — AdaptyPaywallProduct:
  product.vendorProductId; // 'premium_monthly'

  switch (purchaseResult) {
    case AdaptyPurchaseResultSuccess(profile: final profile):
      // profile — AdaptyProfile:
      profile.accessLevels['premium']?.isActive;  // true
      profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30)
      break;
    case AdaptyPurchaseResultPending():
      // no additional data
      break;
    case AdaptyPurchaseResultUserCancelled():
      // no additional data
      break;
  }
}
```
</Details>

We recommend dismissing the screen in that case. Refer to [Respond to button actions](flutter-handle-paywall-actions) for details on dismissing a paywall screen.

#### Finished web payment navigation

This method is invoked after an attempt to open a [web paywall](web-paywall) for a specific product. This includes both successful and failed navigation attempts:

```dart showLineNumbers title="Flutter"
void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, 
                                               AdaptyPaywallProduct? product, 
                                               AdaptyError? error) {
}
```

**Parameters:**

| Parameter   | Description                                                                                        |
|:------------|:---------------------------------------------------------------------------------------------------|
| **product** | An `AdaptyPaywallProduct` for which the web paywall was opened. Can be `null`.                     |
| **error**   | An `AdaptyError` object if the web paywall navigation failed; `null` if navigation was successful. |

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

```dart
void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view,
                                               AdaptyPaywallProduct? product,
                                               AdaptyError? error) {
  // product — AdaptyPaywallProduct?:
  product?.vendorProductId; // 'premium_monthly'

  if (error == null) {
    // navigation succeeded
  } else {
    // error — AdaptyError:
    error.code;    // AdaptyErrorCode.networkFailed (2005)
    error.message; // 'Network request failed'
    error.detail;  // platform-specific underlying error, or null
  }
}
```
</Details>

#### Failed purchase

This method is invoked when a purchase fails (for example, due to payment issues or network errors). It does **not** fire for user-initiated cancellations or pending transactions—those are handled by `paywallViewDidFinishPurchase`:

```dart showLineNumbers title="Flutter"
void paywallViewDidFailPurchase(AdaptyUIPaywallView view, 
                                AdaptyPaywallProduct product, 
                                AdaptyError error) {
}
```

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

```dart
void paywallViewDidFailPurchase(AdaptyUIPaywallView view,
                                AdaptyPaywallProduct product,
                                AdaptyError error) {
  // product — AdaptyPaywallProduct:
  product.vendorProductId; // 'premium_monthly'

  // error — AdaptyError:
  error.code;    // AdaptyErrorCode.productPurchaseFailed (1006)
  error.message; // 'Product purchase failed.'
  error.detail;  // platform-specific underlying error, or null
}
```
</Details>

#### Started restore

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

```dart showLineNumbers title="Flutter"
void paywallViewDidStartRestore(AdaptyUIPaywallView view) {
}
```

#### Successful restore

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

```dart showLineNumbers title="Flutter"
void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) {
}
```

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

```dart
void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) {
  // profile — AdaptyProfile:
  profile.accessLevels['premium']?.isActive;            // true
  profile.accessLevels['premium']?.expiresAt;           // DateTime(2027, 2, 15, 10, 30)
  profile.subscriptions['premium_monthly']?.isActive;   // true
  profile.subscriptions['premium_monthly']?.expiresAt;  // DateTime(2027, 2, 15, 10, 30)
}
```
</Details>

We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](flutter-listen-subscription-changes) topic to learn how to check it and to [Respond to button actions](flutter-handle-paywall-actions) topic to learn how to dismiss a paywall screen.

#### Failed restore

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

```dart showLineNumbers title="Flutter"
void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) {
}
```

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

```dart
void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) {
  // error — AdaptyError:
  error.code;    // AdaptyErrorCode.receiveRestoredTransactionsFailed (1011)
  error.message; // 'Error occurred in the process of restoring purchases.'
  error.detail;  // platform-specific underlying error, or null
}
```
</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 invoking this method:

```dart showLineNumbers title="Flutter"
void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) {
}
```

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

```dart
void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) {
  // error — AdaptyError:
  error.code;    // AdaptyErrorCode.productRequestFailed (1002)
  error.message; // 'Unable to fetch available In-App Purchase products at the moment.'
  error.detail;  // platform-specific underlying error, or null
}
```
</Details>

#### Rendering errors

If an error occurs during the interface rendering, it will be reported by calling this method. By default (since v3.15.2), the paywall is automatically dismissed when a rendering error occurs, but you can override this behavior if needed.

```dart showLineNumbers title="Flutter"
void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) {
  // Default behavior: view.dismiss()
  // Override with custom logic if needed, for example:
  // - Log the error
  // - Show an error message to the user
}
```

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

```dart
void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) {
  // error — AdaptyError:
  error.code;    // AdaptyErrorCode.jsException (4105)
  error.message; // 'An exception was thrown from JS during AdaptyUI flow execution.'
  error.detail;  // platform-specific underlying error, or null

  // Default behavior: view.dismiss()
}
```
</Details>

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

---