Handle flow & paywall events - Android
This guide covers event handling for purchases, restorations, product selection, and flow rendering. You must also implement button handling (closing the flow, opening links, etc.). See our guide on handling button actions for details.
Flows and paywalls configured with the Flow Builder or 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. Learn how to respond to these events below.
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our sample apps, which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
If you need to control or monitor the processes that take place on the purchase screen, implement the AdaptyFlowEventListener methods.
If you would like to leave the default behavior in some cases, you can extend AdaptyFlowDefaultEventListener and override only those methods you want to change.
Below are the defaults from AdaptyFlowDefaultEventListener.
User-generated events
Product selection
If a product is selected for purchase (by a user or by the system), this method will be invoked:
public override fun onProductSelected(
product: AdaptyPaywallProduct,
context: Context,
) {}Event example (Click to expand)
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}Started purchase
If a user initiates the purchase process, this method will be invoked:
public override fun onPurchaseStarted(
product: AdaptyPaywallProduct,
context: Context,
) {}Event example (Click to expand)
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}The method will not be invoked in Observer mode. Refer to the Android - Present Paywall Builder paywalls in Observer mode topic for details.
Successful, canceled, or pending purchase
If purchase succeeds, this method will be invoked:
public override fun onPurchaseFinished(
purchaseResult: AdaptyPurchaseResult,
product: AdaptyPaywallProduct,
context: Context,
) {
if (purchaseResult !is AdaptyPurchaseResult.UserCanceled)
context.getActivityOrNull()?.onBackPressed()
}Event examples (Click to expand)
// Successful purchase
{
"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"
}
}
// Cancelled purchase
{
"purchaseResult": {
"type": "UserCanceled"
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
// Pending purchase
{
"purchaseResult": {
"type": "Pending"
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}We recommend dismissing the screen in that case.
The method will not be invoked in Observer mode. Refer to the Android - 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 Google Play Billing errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger onPurchaseFinished with a cancelled result instead, and pending payments do not trigger this method.
public override fun onPurchaseFailure(
error: AdaptyError,
product: AdaptyPaywallProduct,
context: Context,
) {}Event example (Click to expand)
{
"error": {
"code": "purchase_failed",
"message": "Purchase failed due to insufficient funds",
"details": {
"underlyingError": "Insufficient funds in account"
}
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}The method will not be invoked in Observer mode. Refer to the Android - Present Paywall Builder paywalls in Observer mode topic for details.
Finished web payment navigation
This method is invoked after an attempt to open a web paywall for a specific product. This includes both successful and failed navigation attempts:
public override fun onFinishWebPaymentNavigation(
product: AdaptyPaywallProduct?,
error: AdaptyError?,
context: Context,
) {}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. |
Event examples (Click to expand)
// Successful navigation
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": null
}
// Failed navigation
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": {
"code": "web_navigation_failed",
"message": "Failed to open web paywall",
"details": {
"underlyingError": "Browser unavailable"
}
}
}Successful restore
If restoring a purchase succeeds, this method will be invoked:
public override fun onRestoreSuccess(
profile: AdaptyProfile,
context: Context,
) {}Event example (Click to expand)
{
"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"
}
]
}
}We recommend dismissing the screen if the user has the required accessLevel. Refer to the Subscription status topic to learn how to check it.
Failed restore
If Adapty.restorePurchases() fails, this method will be invoked:
public override fun onRestoreFailure(
error: AdaptyError,
context: Context,
) {}Event example (Click to expand)
{
"error": {
"code": "restore_failed",
"message": "Purchase restoration failed",
"details": {
"underlyingError": "No previous purchases found"
}
}
}Upgrade subscription
When a user attempts to purchase a new subscription while another subscription is active, you can control how the new purchase should be handled by overriding this method. You have two options:
- Replace the current subscription with the new one:
public override fun onAwaitingPurchaseParams(
product: AdaptyPaywallProduct,
context: Context,
onPurchaseParamsReceived: AdaptyFlowEventListener.PurchaseParamsCallback,
): AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked {
onPurchaseParamsReceived(
AdaptyPurchaseParameters.Builder()
.withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...))
.build()
)
return AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked
}- Keep both subscriptions (add the new one separately):
public override fun onAwaitingPurchaseParams(
product: AdaptyPaywallProduct,
context: Context,
onPurchaseParamsReceived: AdaptyFlowEventListener.PurchaseParamsCallback,
): AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked {
onPurchaseParamsReceived(AdaptyPurchaseParameters.Empty)
return AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked
}If you don’t override this method, the default behavior is to keep both subscriptions active (equivalent to using AdaptyPurchaseParameters.Empty).
You can also set additional purchase parameters if needed:
AdaptyPurchaseParameters.Builder()
.withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) // optional - for replacing current subscription
.withOfferPersonalized(true) // optional - if using personalized pricing
.build()Event example (Click to expand)
{
"product": {
"vendorProductId": "premium_yearly",
"localizedTitle": "Premium Yearly",
"localizedDescription": "Premium subscription for 1 year",
"localizedPrice": "$99.99",
"price": 99.99,
"currencyCode": "USD"
},
"subscriptionUpdateParams": {
"replacementMode": "with_time_proration"
}
}Data fetching and rendering
Product loading errors
If you don’t pass the products 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:
public override fun onLoadingProductsFailure(
error: AdaptyError,
context: Context,
): Boolean = falseEvent example (Click to expand)
{
"error": {
"code": "products_loading_failed",
"message": "Failed to load products from the server",
"details": {
"underlyingError": "Network timeout"
}
}
}If you return true, AdaptyUI will repeat the request in 2 seconds.
Rendering errors
If an error occurs during the interface rendering, it will be reported by calling this method:
public override fun onError(
error: AdaptyError,
context: Context,
) {}Event example (Click to expand)
{
"error": {
"code": "rendering_failed",
"message": "Failed to render flow interface",
"details": {
"underlyingError": "Invalid flow configuration"
}
}
}In a normal situation, such errors should not occur, so if you come across one, please let us know.
Navigation
System back button
By default, a flow can’t be dismissed with the system back button or back gesture — the user leaves it through a path you define, such as a Close button or an on_device_back action in the builder. If you want the system back button to dismiss the flow, override onBackPressed and return false to let your host activity or fragment handle the press:
public override fun onBackPressed(context: Context): Boolean {
return false // let the host handle the back press (e.g. finish the activity or pop the fragment)
}This callback is invoked only when no on_device_back action is configured for the current screen — a configured action takes precedence and is handled internally. Return true to consume the press (the default behavior), or false to let the host’s own back handling run.
Reserved events
AdaptyFlowEventListener declares a few callbacks for functionality that flows don’t use yet. You don’t need to implement them — AdaptyFlowDefaultEventListener already provides no-op defaults.
| Method | Description |
|---|---|
| onAnalyticEvent | 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. |
| onShowAppRate | Reserved for app-review requests from a flow. Flows don’t trigger app-review requests yet, so you don’t need to implement it. |
| onShowRequestPermission | Reserved 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. |
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.
This guide is for new Paywall Builder paywalls only which require Adapty SDK v3.0 or later.
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our sample apps, which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
If you need to control or monitor the processes that take place on the purchase screen, implement the AdaptyUiEventListener methods.
If you would like to leave the default behavior in some cases, you can extend AdaptyUiDefaultEventListener and override only those methods you want to change.
Below are the defaults from AdaptyUiDefaultEventListener.
User-generated events
Product selection
If a product is selected for purchase (by a user or by the system), this method will be invoked:
public override fun onProductSelected(
product: AdaptyPaywallProduct,
context: Context,
) {}Event example (Click to expand)
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}Started purchase
If a user initiates the purchase process, this method will be invoked:
public override fun onPurchaseStarted(
product: AdaptyPaywallProduct,
context: Context,
) {}Event example (Click to expand)
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}The method will not be invoked in Observer mode. Refer to the Android - Present Paywall Builder paywalls in Observer mode topic for details.
Successful, canceled, or pending purchase
If purchase succeeds, this method will be invoked:
public override fun onPurchaseFinished(
purchaseResult: AdaptyPurchaseResult,
product: AdaptyPaywallProduct,
context: Context,
) {
if (purchaseResult !is AdaptyPurchaseResult.UserCanceled)
context.getActivityOrNull()?.onBackPressed()
}Event examples (Click to expand)
// Successful purchase
{
"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"
}
}
// Cancelled purchase
{
"purchaseResult": {
"type": "UserCanceled"
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
// Pending purchase
{
"purchaseResult": {
"type": "Pending"
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}We recommend dismissing the screen in that case.
The method will not be invoked in Observer mode. Refer to the Android - 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 Google Play Billing errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger onPurchaseFinished with a cancelled result instead, and pending payments do not trigger this method.
public override fun onPurchaseFailure(
error: AdaptyError,
product: AdaptyPaywallProduct,
context: Context,
) {}Event example (Click to expand)
{
"error": {
"code": "purchase_failed",
"message": "Purchase failed due to insufficient funds",
"details": {
"underlyingError": "Insufficient funds in account"
}
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}The method will not be invoked in Observer mode. Refer to the Android - Present Paywall Builder paywalls in Observer mode topic for details.
Finished web payment navigation
This method is invoked after an attempt to open a web paywall for a specific product. This includes both successful and failed navigation attempts:
public override fun onFinishWebPaymentNavigation(
product: AdaptyPaywallProduct?,
error: AdaptyError?,
context: Context,
) {}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. |
Event examples (Click to expand)
// Successful navigation
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": null
}
// Failed navigation
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": {
"code": "web_navigation_failed",
"message": "Failed to open web paywall",
"details": {
"underlyingError": "Browser unavailable"
}
}
}Successful restore
If restoring a purchase succeeds, this method will be invoked:
public override fun onRestoreSuccess(
profile: AdaptyProfile,
context: Context,
) {}Event example (Click to expand)
{
"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"
}
]
}
}We recommend dismissing the screen if the user has the required accessLevel. Refer to the Subscription status topic to learn how to check it.
Failed restore
If Adapty.restorePurchases() fails, this method will be invoked:
public override fun onRestoreFailure(
error: AdaptyError,
context: Context,
) {}Event example (Click to expand)
{
"error": {
"code": "restore_failed",
"message": "Purchase restoration failed",
"details": {
"underlyingError": "No previous purchases found"
}
}
}Upgrade subscription
Event example (Click to expand)
{
"product": {
"vendorProductId": "premium_yearly",
"localizedTitle": "Premium Yearly",
"localizedDescription": "Premium subscription for 1 year",
"localizedPrice": "$99.99",
"price": 99.99,
"currencyCode": "USD"
},
"subscriptionUpdateParams": {
"replacementMode": "with_time_proration"
}
}Data fetching and rendering
Product loading errors
If you don’t pass the products 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:
public override fun onLoadingProductsFailure(
error: AdaptyError,
context: Context,
): Boolean = falseEvent example (Click to expand)
{
"error": {
"code": "products_loading_failed",
"message": "Failed to load products from the server",
"details": {
"underlyingError": "Network timeout"
}
}
}If you return true, AdaptyUI will repeat the request in 2 seconds.
Rendering errors
If an error occurs during the interface rendering, it will be reported by calling this method:
public override fun onRenderingError(
error: AdaptyError,
context: Context,
) {}Event example (Click to expand)
{
"error": {
"code": "rendering_failed",
"message": "Failed to render paywall interface",
"details": {
"underlyingError": "Invalid paywall configuration"
}
}
}In a normal situation, such errors should not occur, so if you come across one, please let us know.