Привет всем, я Влад, core разработчик Android SDK для обработки платежей в мобильных приложениях в Adapty Это вторая статья из нашего цикла о реализации покупок на Android. В первой статье мы рассказывали о том, как создавать продукты в Google Play Console, сконфигурировать подписки и получить список продуктов в приложении. Cоветую познакомиться и с остальными: Android in-app purchases, часть 1: конфигурация и добавление в проект. Android in-app purchases, часть 2: инициализация и обработка покупок.  — Вы тут Android in-app purchases, часть 3: получение активных покупок и смена подписки. Android in-app purchases, часть 4: коды ошибок от Billing Library и как не облажаться с тестированием. Android in-app purchases, часть 5: серверная валидация покупок. В прошлой части мы создали класс-обертку для работы с Billing Library: Перейдем дальше к реализации покупки и дополним наш класс. В любом приложении, которое использует встроенные покупки, присутствует пейволл. Есть требования от Google, которые определяют минимальный набор необходимых элементов и поясняющих текстов для подобных экранов. Если коротко, на пейволле вы должны прозрачно показать пользователю условия, цену и длительность подписки и обязательна ли подписка для использования приложения. Нельзя принуждать пользователя к дополнительным действиям ради получения информации об условиях подписки. На данном этапе для примера мы сделали упрощённый вариант пейволла: Итак, на нашем пейволле располагаются следующие элементы: Заголовок. Набор кнопок для инициализации процесса покупки. На них указаны основные свойства продуктов: название и цена в местной валюте. Поясняющий текст. Кнопка восстановления прошлых покупок. Этот элемент необходим для всех приложений, в которых используются подписки либо non-consumable покупки. В нашем примере четыре продукта: две автовозобновляемые подписки ("premium_sub_month" и "premium_sub_year"); продукт, который нельзя купить повторно, — non-consumable (“unlock_feature”); продукт, который можно покупать много раз, — consumable (“coin_pack_large”). Для упрощения примера будем использовать Activity, в которую заинжектим BillingClientWrapper из предыдущей статьи, и layout с жестко заданным количеством кнопок для покупки. Для удобства добавим словарь, где ключом является sku продукта, а значением - соответствующая кнопка на экране. Объявим метод для отображения продуктов в UI, опираясь на логику из предыдущей статьи: product.price — это уже отформатированная строка с указанием местной валюты для данного аккаунта, здесь никакое дополнительное форматирование не нужно; остальные поля в объекте класса SkuDetails также приходят уже с учетом локализации. Для проведения покупки необходимо вызвать метод launchBillingFlow() из главного потока приложения. Добавим для этого метод purchase() в BillingClientWrapper: У метода launchBillingFlow() нет колбэка, ответ вернется в метод onPurchasesUpdated(). Помните, мы в прошлой статье его объявили, но оставили на потом? Вот сейчас он нам понадобится. Метод onPurchasesUpdated() вызывается при каком-либо результате после взаимодействия пользователя с диалогом покупки. Это может быть успешная покупка, отмена покупки (пользователь закрыл диалог, в этом случае приходит код BillingResponseCode.USER_CANCELED) или же какая-то другая ошибка. По аналогии с интерфейсом OnQueryProductsListener из предыдущей статьи, объявим в классе BillingClientWrapper интерфейс OnPurchaseListener, с помощью которого будем получать либо покупку (объект класса Purchase — о кейсе, когда он может быть null даже в случае успеха, расскажем в следующей статье), либо ошибку, которую мы также объявляли в предыдущей статье: И реализуем его в PaywallActivity: Добавим логику в onPurchaseUpdated(): Если purchaseList не пустой, для начала для каждой покупки делаем проверку, что ее purchasedState равен PurchaseState.PURCHASED, потому что покупки также могут быть отложенными, и в этом случае флоу на данном этапе прекращается. Далее, согласно документации, нужно сделать серверную верификацию покупки, о ней мы расскажем в следующих статьях. После этого надо предоставить пользователю доступ к контенту и сообщить об этом в Google. Если не сообщить, то через три дня покупка автоматически отменится. Интересно, что это характерно только для Google Play, в то время как на iOS такого нет. Сообщить о предоставлении доступа к контенту можно двумя способами: с помощью acknowledgePurchase() со стороны клиента; либо Purchases.Products.Acknowledge/Purchases.Subscriptions.Acknowledge со стороны бэка.  В случае с consumable-продуктом вместо него нужно вызвать метод consumeAsync(), который под капотом делает acknowledge, а заодно дает возможность покупать этот продукт повторно. Это можно сделать только с помощью Billing Library: Google Play Developer API почему-то не предоставляет возможность делать это на бэкенде. Любопытно, что, в отличие от Google Play, в App Store и в AppGallery свойство consumable за продуктом жестко определено на уровне App Store Connect и AppGallery Connect соответственно. Справедливости ради, замечу, что консьюмить такие продукты из AppGallery нужно всё-таки явно. Напишем методы для acknowledge и consume, а также две версии метода processPurchase() — в случае, когда у нас есть свой бэкенд и когда его нет. Без серверной верификации: С серверной верификацией: Подробнее о серверной верификации покупок мы расскажем в одной из следующих статей. Во втором примере acknowledge, конечно, тоже можно было сделать на клиенте, но так как здесь у нас есть бэкенд, всё, что можно сделать на бэке, лучше отдать бэку. Что касается ошибок на acknowledge и consume, их нельзя игнорировать, потому что если ни одно из этих действий не произойдет в течение трёх дней после того, как покупка получила статус PurchaseState.PURCHASED, она отменится, а пользователю вернут средства. Поэтому, если мы не можем это сделать на бэкенде, и даже после нескольких повторных попыток всё еще получаем ошибку, самый надежный способ —  получать текущие покупки пользователя в каком-нибудь методе жизненного цикла, например в onStart() или onResume(), и пытаться повторить в надежде, что пользователь в течение трёх дней зайдет в наше приложение при работающем интернете :). Таким образом, текущая версия класса BillingClientWrapper будет выглядеть так: Вы могли задаться вопросом, почему кнопки активны для всех продуктов, независимо от того, покупал их пользователь или нет. Или что будет, если купить обе подписки: заменит ли вторая первую или они обе будут сосуществовать. Всё это в следующих статьях :) Cоветую познакомиться с Adapty — SDK для встроенных покупок.  Он не только упрощает работу по добавлению покупок: Встроенная аналитика позволяет быстро понять основные метрики приложения. Когортный анализ отвечает на вопрос, как быстро сходится экономика. А/Б тесты увеличивают выручку приложения. Интеграции с внешними системами позволяют отправлять транзакции в сервисы атрибуции и продуктовой аналитики. Промо-кампании уменьшают отток аудитории. Open source SDK позволяет интегрировать подписки в приложение за несколько часов. Серверная валидация и API для работы с другими платформами. Познакомьтесь подробнее с этими возможностями, чтобы быстрее внедрить подписки в своё приложение и улучшить конверсии. Android dev