Обработка событий флоу и пейвола - Kotlin Multiplatform

Этот гайд охватывает обработку событий покупок, восстановлений, выбора продуктов и рендеринга флоу. Вам также необходимо реализовать обработку кнопок (закрытие флоу, открытие ссылок и т.д.). Подробнее смотрите в нашем гайде по обработке действий флоу.

Флоу и пейволы, настроенные с помощью Flow Builder или Paywall Builder, не требуют дополнительного кода для совершения и восстановления покупок. Однако они генерируют события, на которые ваше приложение может реагировать. К таким событиям относятся нажатия кнопок (кнопки закрытия, URL-адреса, выбор продуктов и т. д.), а также уведомления о действиях, связанных с покупками. Узнайте, как реагировать на эти события, ниже.

Чтобы управлять процессами на экране флоу или отслеживать их в своём мобильном приложении, реализуйте методы интерфейса AdaptyUIFlowsEventsObserver и зарегистрируйте наблюдатель через AdaptyUI.setFlowsEventsObserver(). Некоторые методы имеют реализации по умолчанию, которые автоматически обрабатывают типичные сценарии, — переопределяйте только те из них, которые нужно изменить:

AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver {
    // override only the methods you want to change
})

Здесь вы добавляете собственную логику для обработки событий флоу. Используйте view.dismiss(), чтобы закрыть флоу, или реализуйте любое другое нужное поведение. Обратите внимание: dismiss() — это suspend-функция, поэтому внутри колбэка запускайте её через mainUiScope обозревателя: mainUiScope.launch { view.dismiss() }.

События, сгенерированные пользователем

Появление и исчезновение флоу

Когда флоу появляется или исчезает, будут вызваны следующие методы:

override fun flowViewDidAppear(view: AdaptyUIFlowView) {
    // Handle flow appearance
    // You can track analytics or update UI here
}

override fun flowViewDidDisappear(view: AdaptyUIFlowView) {
    // Handle flow disappearance
    // You can track analytics or update UI here
}
  • На iOS flowViewDidAppear также вызывается, когда пользователь нажимает кнопку веб-пейвола внутри флоу, и веб-пейвол открывается во встроенном браузере.
  • На iOS flowViewDidDisappear также вызывается, когда веб-пейвол, открытый из флоу во встроенном браузере, исчезает с экрана.
Примеры событий (нажмите, чтобы развернуть)
// Flow appeared
{
  // No additional data
}

// Flow disappeared
{
  // No additional data
}

Выбор продукта

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

override fun flowViewDidSelectProduct(view: AdaptyUIFlowView, productId: String) {
    // Handle product selection
    // You can update UI or track analytics here
}
Пример события (нажмите, чтобы развернуть)
{
  "productId": "premium_monthly"
}

Покупка начата

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

override fun flowViewDidStartPurchase(view: AdaptyUIFlowView, product: AdaptyPaywallProduct) {
    // Handle purchase start
    // You can show loading indicators or track analytics here
}

В режиме Observer, покупки, инициированные из флоу, передаются в ваш AdaptyUIObserverModeResolver.

Пример события (нажмите, чтобы развернуть)
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  }
}

Успешная, отменённая или ожидающая покупка

Этот метод вызывается после завершения покупки. По умолчанию он ничего не делает — флоу остаётся открытым после покупки, пока вы его явно не закроете, поэтому вызывайте view.dismiss() самостоятельно, как только пользователь получает доступ:

override fun flowViewDidFinishPurchase(
    view: AdaptyUIFlowView,
    product: AdaptyPaywallProduct,
    purchaseResult: AdaptyPurchaseResult
) {
    when (purchaseResult) {
        is AdaptyPurchaseResult.Success -> {
            // Check if user has access to premium features
            if (purchaseResult.profile.accessLevels["premium"]?.isActive == true) {
                mainUiScope.launch { view.dismiss() }
            }
        }
        AdaptyPurchaseResult.Pending -> {
            // Handle pending purchase (e.g., user will pay offline with cash)
        }
        AdaptyPurchaseResult.UserCanceled -> {
            // Handle user cancellation
        }
    }
}
Примеры событий (нажмите, чтобы развернуть)
// 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"
        }
      }
    }
  }
}

// Pending purchase
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "purchaseResult": {
    "type": "Pending"
  }
}

// User canceled purchase
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "purchaseResult": {
    "type": "UserCanceled"
  }
}

Рекомендуем закрывать экран флоу при успешной покупке.

Неудачная покупка

Этот метод вызывается, если покупка завершается с ошибкой. Сюда входят ошибки StoreKit/Google Play Billing (ограничения платежей, недействительные продукты, сетевые сбои), ошибки верификации транзакций и системные ошибки. Обратите внимание: отмена покупки пользователем вызывает flowViewDidFinishPurchase с результатом отмены, а ожидающие платежи этот метод не вызывают.

override fun flowViewDidFailPurchase(
    view: AdaptyUIFlowView,
    product: AdaptyPaywallProduct,
    error: AdaptyError
) {
    // Add your purchase failure handling logic here
    // For example: show error message, retry option, or custom error handling
}
Пример события (нажмите, чтобы развернуть)
{
  "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"
    }
  }
}

Восстановление начато

Если пользователь инициирует процесс восстановления покупок, будет вызван этот метод:

override fun flowViewDidStartRestore(view: AdaptyUIFlowView) {
    // Handle restore start
    // You can show loading indicators or track analytics here
}

Успешное восстановление

Если восстановление покупки прошло успешно, будет вызван этот метод. По умолчанию он ничего не делает — флоу остаётся открытым после восстановления, пока вы его не закроете:

override fun flowViewDidFinishRestore(view: AdaptyUIFlowView, profile: AdaptyProfile) {
    // Add your successful restore handling logic here
    // For example: show success message, update UI, or dismiss the flow

    // Check if user has access to premium features
    if (profile.accessLevels["premium"]?.isActive == true) {
        mainUiScope.launch { view.dismiss() }
    }
}
Пример события (нажмите, чтобы раскрыть)
{
  "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"
      }
    ]
  }
}

Рекомендуем закрывать экран, если у пользователя есть нужный accessLevel. О том, как это проверить, читайте в разделе Статус подписки.

Неудачное восстановление

Если Adapty.restorePurchases() завершится с ошибкой, будет вызван этот метод:

override fun flowViewDidFailRestore(view: AdaptyUIFlowView, error: AdaptyError) {
    // Add your restore failure handling logic here
    // For example: show error message, retry option, or custom error handling
}
Пример события (нажмите, чтобы развернуть)
{
  "error": {
    "code": "restore_failed",
    "message": "Purchase restoration failed",
    "details": {
      "underlyingError": "No previous purchases found"
    }
  }
}

Завершение навигации веб-оплаты

Если пользователь инициирует процесс покупки через веб-пейвол, будет вызван этот метод:

override fun flowViewDidFinishWebPaymentNavigation(
    view: AdaptyUIFlowView,
    product: AdaptyPaywallProduct?,
    error: AdaptyError?
) {
    if (error != null) {
        // Handle web payment navigation error
    } else {
        // Handle successful web payment navigation
    }
}
Примеры событий (нажмите, чтобы развернуть)
// Successful web payment 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 web payment navigation
{
  "product": null,
  "error": {
    "code": "web_payment_failed",
    "message": "Web payment navigation failed",
    "details": {
      "underlyingError": "Network connection error"
    }
  }
}

Загрузка данных и рендеринг

Ошибки загрузки продуктов

Если вы не передаёте продукты при инициализации, AdaptyUI самостоятельно получит необходимые объекты с сервера. Если эта операция завершится неудачей, AdaptyUI сообщит об ошибке, вызвав следующий метод:

override fun flowViewDidFailLoadingProducts(view: AdaptyUIFlowView, error: AdaptyError) {
    // Add your product loading failure handling logic here
    // For example: show error message, retry option, or custom error handling
}
Пример события (нажмите, чтобы развернуть)
{
  "error": {
    "code": "products_loading_failed",
    "message": "Failed to load products from the server",
    "details": {
      "underlyingError": "Network timeout"
    }
  }
}

Ошибки рендеринга и ошибки во время выполнения

Если в процессе рендеринга интерфейса или во время выполнения возникает ошибка (не связанная с покупкой), она будет передана через этот метод. По умолчанию флоу закрывается при ошибке — переопределите метод, чтобы оставить его открытым или добавить собственную обработку:

override fun flowViewDidReceiveError(view: AdaptyUIFlowView, error: AdaptyError) {
    // Handle the error
    // The default implementation dismisses the flow;
    // once you override this method, dismissal is up to you
}
Пример события (нажмите, чтобы развернуть)
{
    "error": {
        "code": "rendering_failed",
            "message": "Failed to render flow interface",
            "details": {
            "underlyingError": "Invalid flow configuration"
        }
    }
}

В штатной ситуации такие ошибки возникать не должны, поэтому, если вы с ними столкнулись, сообщите нам.

События аналитики

Коллбэк flowViewDidReceiveAnalyticEvent зарезервирован для пользовательских событий аналитики из флоу. Флоу пока не отправляют такие события в ваш код, поэтому реализовывать его не нужно:

override fun flowViewDidReceiveAnalyticEvent(
    view: AdaptyUIFlowView,
    name: String,
    paramsJsonString: String
) {
    // Reserved for custom analytic events from a flow
}

Кнопка «Назад» на Android

По умолчанию флоу нельзя закрыть системной кнопкой «Назад» или жестом назад на Android — стандартная реализация flowViewDidPerformAction закрывает флоу только по CloseAction и игнорирует AndroidSystemBackAction, поэтому пользователь покидает флоу тем путём, который вы определили: через кнопку Close или действие on_device_back в билдере. Если вы хотите, чтобы системная кнопка «Назад» закрывала флоу, обработайте это действие самостоятельно:

override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) {
    when (action) {
        is AdaptyUIAction.CloseAction ->
            mainUiScope.launch { view.dismiss() } // default behavior
        is AdaptyUIAction.AndroidSystemBackAction ->
            mainUiScope.launch { view.dismiss() } // not handled by default
        is AdaptyUIAction.OpenUrlAction ->
            AdaptyUI.openWebUrl(action.url, action.openIn) // default behavior
        else -> Unit
    }
}

См. гайд по обработке действий флоу для полного списка действий.

Пейволы, настроенные с помощью Paywall Builder, не требуют дополнительного кода для совершения и восстановления покупок. Однако они генерируют события, на которые ваше приложение может реагировать. К ним относятся нажатия кнопок (кнопки закрытия, URL-адреса, выбор продуктов и т. д.), а также уведомления о действиях, связанных с покупками, на пейволе. Узнайте, как реагировать на эти события, ниже.

Этот гайд предназначен только для пейволов нового Paywall Builder.

Для управления или отслеживания событий на экране пейвола в вашем мобильном приложении реализуйте методы интерфейса AdaptyUIPaywallsEventsObserver. Некоторые методы имеют реализации по умолчанию, которые автоматически обрабатывают типичные сценарии.

В этих методах вы добавляете собственную логику для реакции на события пейвола. Чтобы закрыть пейвол, используйте view.dismiss(), либо реализуйте любое другое необходимое поведение.

События, генерируемые пользователями

Появление и исчезновение пейвола

Когда пейвол появляется или исчезает, вызываются следующие методы:

override fun paywallViewDidAppear(view: AdaptyUIPaywallView) {
    // Handle paywall appearance
    // You can track analytics or update UI here
}

override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) {
    // Handle paywall disappearance
    // You can track analytics or update UI here
}
  • На iOS paywallViewDidAppear также вызывается, когда пользователь нажимает на кнопку веб-пейвола внутри пейвола, и веб-пейвол открывается во встроенном браузере.
  • На iOS paywallViewDidDisappear также вызывается, когда веб-пейвол, открытый из пейвола во встроенном браузере, исчезает с экрана.
Примеры событий (нажмите, чтобы развернуть)
// Paywall appeared
{
  // No additional data
}

// Paywall disappeared
{
  // No additional data
}

Выбор продукта

Если пользователь выбирает продукт для покупки, будет вызван этот метод:

override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) {
    // Handle product selection
    // You can update UI or track analytics here
}
Пример события (нажмите, чтобы развернуть)
{
  "productId": "premium_monthly"
}

Начало покупки

Если пользователь начинает процесс покупки, будет вызван этот метод:

override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) {
    // Handle purchase start
    // You can show loading indicators or track analytics here
}
Пример события (нажмите, чтобы развернуть)
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  }
}

Успешная, отменённая или ожидающая покупка

Если покупка прошла успешно, будет вызван этот метод. По умолчанию он автоматически закрывает пейвол, если только покупка не была отменена пользователем:

override fun paywallViewDidFinishPurchase(
    view: AdaptyUIPaywallView,
    product: AdaptyPaywallProduct,
    purchaseResult: AdaptyPurchaseResult
) {
    when (purchaseResult) {
        is AdaptyPurchaseResult.Success -> {
            // Check if user has access to premium features
            if (purchaseResult.profile.accessLevels["premium"]?.isActive == true) {
                view.dismiss()
            }
        }
        AdaptyPurchaseResult.Pending -> {
            // Handle pending purchase (e.g., user will pay offline with cash)
        }
        AdaptyPurchaseResult.UserCanceled -> {
            // Handle user cancellation
        }
    }
}
Примеры событий (нажмите, чтобы развернуть)
// 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"
        }
      }
    }
  }
}

// Pending purchase
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "purchaseResult": {
    "type": "Pending"
  }
}

// User canceled purchase
{
  "product": {
    "vendorProductId": "premium_monthly",
    "localizedTitle": "Premium Monthly",
    "localizedDescription": "Premium subscription for 1 month",
    "localizedPrice": "$9.99",
    "price": 9.99,
    "currencyCode": "USD"
  },
  "purchaseResult": {
    "type": "UserCanceled"
  }
}

Рекомендуем закрывать экран пейвола после успешной покупки.

Неудачная покупка

Если покупка завершилась ошибкой, будет вызван этот метод. Сюда входят ошибки StoreKit/Google Play Billing (ограничения оплаты, недействительные продукты, сбои сети), ошибки верификации транзакций и системные ошибки. Обратите внимание, что отмена покупки пользователем вызывает paywallViewDidFinishPurchase с результатом отмены, а ожидающие платежи этот метод не вызывают.

override fun paywallViewDidFailPurchase(
    view: AdaptyUIPaywallView,
    product: AdaptyPaywallProduct,
    error: AdaptyError
) {
    // Add your purchase failure handling logic here
    // For example: show error message, retry option, or custom error handling
}
Пример события (нажмите, чтобы развернуть)
{
  "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"
    }
  }
}

Начало восстановления

Если пользователь инициирует процесс восстановления, будет вызван этот метод:

override fun paywallViewDidStartRestore(view: AdaptyUIPaywallView) {
    // Handle restore start
    // You can show loading indicators or track analytics here
}

Успешное восстановление покупки

Если восстановление покупки прошло успешно, будет вызван следующий метод:

override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) {
    // Add your successful restore handling logic here
    // For example: show success message, update UI, or dismiss paywall
    
    // Check if user has access to premium features
    if (profile.accessLevels["premium"]?.isActive == true) {
        view.dismiss()
    }
}
Пример события (нажмите, чтобы развернуть)
{
  "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"
      }
    ]
  }
}

Рекомендуем закрывать экран, если у пользователя есть нужный accessLevel. Подробнее о том, как это проверить, читайте в разделе Статус подписки.

Ошибка восстановления покупок

Если Adapty.restorePurchases() завершается с ошибкой, будет вызван этот метод:

override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) {
    // Add your restore failure handling logic here
    // For example: show error message, retry option, or custom error handling
}
Пример события (нажмите, чтобы развернуть)
{
  "error": {
    "code": "restore_failed",
    "message": "Purchase restoration failed",
    "details": {
      "underlyingError": "No previous purchases found"
    }
  }
}

Завершение навигации веб-оплаты

Если пользователь инициирует процесс покупки через веб-пейвол, будет вызван этот метод:

override fun paywallViewDidFinishWebPaymentNavigation(
    view: AdaptyUIPaywallView,
    product: AdaptyPaywallProduct?,
    error: AdaptyError?
) {
    if (error != null) {
        // Handle web payment navigation error
    } else {
        // Handle successful web payment navigation
    }
}
Примеры событий (нажмите, чтобы развернуть)
// Successful web payment 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 web payment navigation
{
  "product": null,
  "error": {
    "code": "web_payment_failed",
    "message": "Web payment navigation failed",
    "details": {
      "underlyingError": "Network connection error"
    }
  }
}

Получение и отображение данных

Ошибки загрузки продуктов

Если вы не передаёте продукты при инициализации, AdaptyUI самостоятельно получит нужные объекты с сервера. Если эта операция завершится неудачей, AdaptyUI сообщит об ошибке, вызвав следующий метод:

override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) {
    // Add your product loading failure handling logic here
    // For example: show error message, retry option, or custom error handling
}
Пример события (нажмите, чтобы развернуть)
{
  "error": {
    "code": "products_loading_failed",
    "message": "Failed to load products from the server",
    "details": {
      "underlyingError": "Network timeout"
    }
  }
}

Ошибки рендеринга

Если в процессе рендеринга интерфейса возникнет ошибка, она будет сообщена этим методом:

override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) {
    // Handle rendering error
    // In a normal situation, such errors should not occur
    // If you come across one, please let us know
}
Пример события (нажмите, чтобы развернуть)
{
    "error": {
        "code": "rendering_failed",
            "message": "Failed to render paywall interface",
            "details": {
            "underlyingError": "Invalid paywall configuration"
        }
    }
}

В штатной ситуации такие ошибки возникать не должны, поэтому если вы столкнулись с одной из них, пожалуйста, сообщите нам.