Handle flow & paywall events - Capacitor

Important

This guide covers event handling for purchases, restorations, product selection, and flow rendering. You can also set up button handling (closing the flow, opening links, custom actions, etc.). See our guide on handling button actions for details.

Flows and paywalls built with the Flow 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. Learn how to respond to these events below.

To control or monitor processes occurring on the flow screen within your mobile app, implement the view.setEventHandlers method:

Important

You can set only one handler per event: calling setEventHandlers multiple times will override the handlers you provide, replacing both default and previously set handlers for those specific events. Handlers you don’t set keep their default behavior. setEventHandlers returns an unsubscribe function, and view.dismiss() clears all handlers.

import { adapty, createFlowView } from '@adapty/capacitor';

const view = await createFlowView(flow);

const unsubscribe = await view.setEventHandlers({
  onCloseButtonPress() {
    return true; // close the flow (default behavior)
  },
  onAndroidSystemBack() {
    return true; // close the flow; by default, it stays open
  },
  onPurchaseCompleted(purchaseResult, product) {
    return purchaseResult.type === 'success'; // close the flow on a successful purchase, keep it open for cancelled or pending purchases
  },
  onPurchaseStarted(product) { /***/ },
  onPurchaseFailed(error, product) { /***/ },
  onRestoreCompleted(profile) { /***/ },
  onRestoreFailed(error) { /***/ },
  onProductSelected(productId) { /***/ },
  onError(error) { /***/ },
  onLoadingProductsFailed(error) { /***/ },
  onUrlPress(url, openIn) {
    adapty.openWebUrl({ url, openIn }).catch(console.warn); // same as the SDK default
    return false; // keep the flow open
  },
  onAppeared(appearedView) { /***/ },
  onDisappeared() { /***/ },
  onWebPaymentNavigationFinished() { /***/ },
});
Event examples (Click to expand)

The examples below show the properties available in each handler, with illustrative values in comments.

// onUrlPress
url;    // 'https://example.com/terms'
openIn; // 'browser_in_app' or 'browser_out_app'

// onCustomAction
actionId; // 'login'

// onProductSelected
productId; // 'premium_monthly'

// onAppeared
appearedView.id;          // '3f8a1c7e-9b24-4d51-8e30-6c5b2a9f1d47'
appearedView.placementId; // 'onboarding_paywall'
appearedView.variationId; // 'd21c4b6a-57e8-4f39-b0a2-8c7e13f5d94b'
appearedView.locale;      // 'es'

// onPurchaseStarted, onPurchaseCompleted, onPurchaseFailed
product.vendorProductId;        // 'premium_monthly'
product.localizedTitle;         // 'Premium Monthly'
product.localizedDescription;   // 'Premium subscription for 1 month'
product.price?.amount;          // 9.99
product.price?.currencyCode;    // 'USD'
product.price?.localizedString; // '$9.99'

// onPurchaseCompleted
purchaseResult.type; // 'success', 'pending', or 'user_cancelled'
if (purchaseResult.type === 'success') {
  purchaseResult.profile.accessLevels['premium']?.isActive; // true
}

// onRestoreCompleted
profile.accessLevels['premium']?.isActive; // true

// onPurchaseFailed, onRestoreFailed, onError, onLoadingProductsFailed
error.message; // 'Purchase failed due to insufficient funds'

You can register event handlers you need, and miss those you do not need. In this case, unused event listeners would not be created. There are no required event handlers.

Event handlers return a boolean. If true is returned, the displaying process is considered complete, thus the flow screen closes and event listeners for this view are removed.

Some event handlers have a default behavior that you can override if needed:

  • onCloseButtonPress: closes the flow when the close button is pressed.
  • onUrlPress: opens the tapped URL in the native browser via adapty.openWebUrl, honoring the Open in option set in the builder, and keeps the flow open.
  • onAndroidSystemBack: keeps the flow open when the Back button is pressed. Return true to close it.
  • onPurchaseCompleted: keeps the flow open after a purchase completes. Return true to close it.
  • onRestoreCompleted: keeps the flow open after a successful restore. Return true to close it.
  • onError: closes the flow if its rendering fails.

Event handlers

Event handlerDescription
onCustomActionInvoked when a user performs a custom action, e.g., clicks a custom button.
onUrlPressInvoked when a user clicks a URL in your flow.
onAndroidSystemBackInvoked when a user taps the system Android Back button. The flow stays open by default; return true to close it.
onCloseButtonPressInvoked when the close button is visible and a user taps it. It is recommended to dismiss the flow screen in this handler.
onPurchaseCompletedInvoked when the purchase completes, whether successful, cancelled by user, or pending approval. In case of a successful purchase, it provides an updated AdaptyProfile. User cancellations and pending payments (e.g., parental approval required) trigger this event, not onPurchaseFailed.
onPurchaseStartedInvoked when a user taps the “Purchase” action button to start the purchase process.
onPurchaseFailedInvoked when a purchase fails due to errors (e.g., payment restrictions, invalid products, network failures, transaction verification failures). Not invoked for user cancellations or pending payments, which trigger onPurchaseCompleted instead.
onRestoreStartedInvoked when a user starts a purchase restoration process.
onRestoreCompletedInvoked when purchase restoration succeeds and provides an updated AdaptyProfile. It is recommended to dismiss the screen if the user has the required accessLevel. Refer to the Subscription status topic to learn how to check it.
onRestoreFailedInvoked when the restore process fails and provides AdaptyError.
onProductSelectedInvoked when any product in the flow view is selected, allowing you to monitor what the user selects before the purchase.
onErrorInvoked when an error occurs during view rendering and provides AdaptyError. Such errors should not occur, so if you come across one, please let us know.
onLoadingProductsFailedInvoked when product loading fails and provides AdaptyError. If you haven’t set prefetchProducts: true in view creation, AdaptyUI will retrieve the necessary objects from the server by itself.
onAppearedInvoked when the flow is displayed to the user, and provides the view that appeared — see The view argument. On iOS, also invoked when a user taps the web paywall button inside a flow, and a web paywall opens in an in-app browser.
onDisappearedInvoked when the flow is closed by the user. On iOS, also invoked when a web paywall opened from a flow in an in-app browser disappears from the screen.
onWebPaymentNavigationFinishedInvoked after attempting to open a web paywall for purchase, whether successful or failed.
onRequestAppReviewReserved for app-review requests from a flow. Flows don’t trigger app-review requests yet, so you don’t need to implement it.
onAnalyticsInvoked when a flow reports an analytics event, such as a screen view. See Analytics events below.
onRequestPermissionReserved for system-permission requests (such as push notifications or camera access) from a flow. Flows don’t trigger permission requests yet, so you don’t need to implement it.
onObserverPurchaseInitiatedObserver mode only: Invoked when a user taps the purchase button in a flow. Adapty does not make the purchase — perform it with your own purchase code, then report the transaction to Adapty. See Handle purchases in observer mode below.
onObserverRestoreInitiatedObserver mode only: Invoked when a user taps the restore button in a flow. Adapty does not restore — perform it yourself, then report any restored transactions. See Handle purchases in observer mode below.

The view argument

The view argument requires Capacitor SDK 4.0.2-beta.1 or later. Of all the flow handlers, only onAppeared receives a description of the view itself — a FlowEventView object with these fields:

FieldDescription
idThe identifier of this view instance. It is internal to the SDK and matches nothing in the Adapty dashboard.
placementIdThe placement the flow was fetched for.
variationIdThe variation the flow resolved to, for attributing your own analytics to an A/B test.
localeThe flow localization the view was built with. It differs from the locale you requested when the flow has no such localization. The view returned by createFlowView reports the same value in its locale property. See Use localizations and locale codes.

Analytics events

view.setEventHandlers({
  onAnalytics(name, params) {
    return false; // keep the flow open
  },
});

A flow reports flow_screen_showed every time a user opens one of its screens. Adapty counts these events in its own flow analytics and delivers them to your app as well, so you can build the same funnel in your own analytics.

ParameterDescription
instanceIdThe ID of the screen the user opened.
screen_orderThe screen’s position in the flow.
is_last_screentrue when the screen has nowhere further to go. A branching flow can end on several different screens, and each one reports true.

Both isBackendEvent and isCustomerEvent are true on this event: Adapty keeps counting it, and your app receives it too.

See Track flow screen views for what to do with them.

Handle purchases in observer mode

If you activated the SDK in Observer mode (observerMode: true) and present an Adapty-rendered flow, the SDK does not make purchases for you. When a user taps the purchase or restore button, the SDK invokes onObserverPurchaseInitiated or onObserverRestoreInitiated instead, so you can perform the purchase or restore with your own code. See Present flows in Observer mode for the full setup.

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 for details.

Paywalls configured with the 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.

To control or monitor processes occurring on the paywall screen within your mobile app, implement the view.setEventHandlers method:

import { adapty, createPaywallView } from '@adapty/capacitor';

const view = await createPaywallView(paywall);

const unsubscribe = view.setEventHandlers({
  onCloseButtonPress() {
    console.log('User closed paywall');
    return true; // Allow the paywall to close
  },
  onAndroidSystemBack() {
    console.log('User pressed back button');
    return true; // Allow the paywall to close
  }, 
  onAppeared() {
    console.log('Paywall appeared');
    return false; // Don't close the paywall
  }, 
  onDisappeared() {
    console.log('Paywall disappeared');
  },
  onPurchaseCompleted(purchaseResult, product) {
    console.log('Purchase completed:', purchaseResult);
    return purchaseResult.type !== 'user_cancelled'; // Close if not cancelled
  },
  onPurchaseStarted(product) {
    console.log('Purchase started:', product);
    return false; // Don't close the paywall
  },
  onPurchaseFailed(error, product) {
    console.error('Purchase failed:', error);
    return false; // Don't close the paywall
  },
  onRestoreCompleted(profile) {
    console.log('Restore completed:', profile);
    return true; // Close the paywall after successful restore
  },
  onRestoreFailed(error) {
    console.error('Restore failed:', error);
    return false; // Don't close the paywall
  },
  onProductSelected(productId) {
    console.log('Product selected:', productId);
    return false; // Don't close the paywall
  },
  onRenderingFailed(error) {
    console.error('Rendering failed:', error);
    return false; // Don't close the paywall
  },
  onLoadingProductsFailed(error) {
    console.error('Loading products failed:', error);
    return false; // Don't close the paywall
  },
  onUrlPress(url) {
    window.open(url, '_blank');
    return false; // Don't close the paywall
  },
});
Event examples (Click to expand)
// onCloseButtonPress
{
  "event": "close_button_press"
}

// onAndroidSystemBack
{
  "event": "android_system_back"
}

// onAppeared
{
  "event": "paywall_shown"
}

// onDisappeared
{
  "event": "paywall_closed"
}

// onUrlPress
{
  "event": "url_press",
  "url": "https://example.com/terms"
}

// onCustomAction
{
  "event": "custom_action",
  "actionId": "login"
}

// onProductSelected
{
  "event": "product_selected",
  "productId": "premium_monthly"
}

// onPurchaseStarted
{
  "event": "purchase_started",
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  }
}

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

// onPurchaseCompleted - Cancelled
{
  "event": "purchase_completed",
  "purchaseResult": {
    "type": "user_cancelled"
  },
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  }
}

// onPurchaseFailed
{
  "event": "purchase_failed",
  "error": {
    "code": "purchase_failed",
    "message": "Purchase failed due to insufficient funds",
    "details": {
      "underlyingError": "Insufficient funds in account"
    }
  }
}

// onRestoreCompleted
{
  "event": "restore_completed",
  "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"
      }
    ]
  }
}

// onRestoreFailed
{
  "event": "restore_failed",
  "error": {
    "code": "restore_failed",
    "message": "Purchase restoration failed",
    "details": {
      "underlyingError": "No previous purchases found"
    }
  }
}

// onRenderingFailed
{
  "event": "rendering_failed",
  "error": {
    "code": "rendering_failed",
    "message": "Failed to render paywall interface",
    "details": {
      "underlyingError": "Invalid paywall configuration"
    }
  }
}

// onLoadingProductsFailed
{
  "event": "loading_products_failed",
  "error": {
    "code": "products_loading_failed",
    "message": "Failed to load products from the server",
    "details": {
      "underlyingError": "Network timeout"
    }
  }
}

You can register event handlers you need, and miss those you do not need. In this case, unused event listeners would not be created. There are no required event handlers.

Event handlers return a boolean. If true is returned, the displaying process is considered complete, thus the paywall screen closes and event listeners for this view are removed.

Some event handlers have a default behavior that you can override if needed:

  • onCloseButtonPress: closes paywall when close button pressed.
  • onAndroidSystemBack: closes paywall when the Back button pressed.
  • onRestoreCompleted: closes paywall after successful restore.
  • onPurchaseCompleted: closes paywall unless user cancelled.
  • onRenderingFailed: closes paywall if its rendering fails.
  • onUrlPress: opens URLs in system browser and keeps paywall open.

Event handlers

Event handlerDescription
onCustomActionInvoked when a user performs a custom action, e.g., clicks a custom button.
onUrlPressInvoked when a user clicks a URL in your paywall.
onAndroidSystemBackInvoked when a user taps the system Android Back button.
onCloseButtonPressInvoked when the close button is visible and a user taps it. It is recommended to dismiss the paywall screen in this handler.
onPurchaseCompletedInvoked when the purchase completes, whether successful, cancelled by user, or pending approval. In case of a successful purchase, it provides an updated AdaptyProfile. User cancellations and pending payments (e.g., parental approval required) trigger this event, not onPurchaseFailed.
onPurchaseStartedInvoked when a user taps the “Purchase” action button to start the purchase process.
onPurchaseCancelledInvoked when a user initiates the purchase process and manually interrupts it (cancels the payment dialog).
onPurchaseFailedInvoked when a purchase fails due to errors (e.g., payment restrictions, invalid products, network failures, transaction verification failures). Not invoked for user cancellations or pending payments, which trigger onPurchaseCompleted instead.
onRestoreStartedInvoked when a user starts a purchase restoration process.
onRestoreCompletedInvoked when purchase restoration succeeds and provides an updated AdaptyProfile. It is recommended to dismiss the screen if the user has the required accessLevel. Refer to the Subscription status topic to learn how to check it.
onRestoreFailedInvoked when the restore process fails and provides AdaptyError.
onProductSelectedInvoked when any product in the paywall view is selected, allowing you to monitor what the user selects before the purchase.
onAppearedInvoked when the paywall view appears on screen. On iOS, also invoked when a user taps the web paywall button inside a paywall, and a web paywall opens in an in-app browser.
onDisappearedInvoked when the paywall view disappears from screen. On iOS, also invoked when a web paywall opened from a paywall in an in-app browser disappears from the screen.
onRenderingFailedInvoked when an error occurs during view rendering and provides AdaptyError. Such errors should not occur, so if you come across one, please let us know.
onLoadingProductsFailedInvoked when product loading fails and provides AdaptyError. If you haven’t set prefetchProducts: true in view creation, AdaptyUI will retrieve the necessary objects from the server by itself.