Доброго времени суток, уважаемый читатель. В статье я хочу рассказать о том, как сделать встроенные покупки и подписки при помощи новой версии библиотеки для биллинга от Google. Статья является гибридом перевода официальной документации с моими комментариями (Официальная документация на английском).
Все примеры будут рассматриваться на языке Java (я работаю именно на нем, не Kotlin).
Преподготовка
Перед тем как приступать к непосредственному написанию кода необходимо подготовить всё в вашей Google Play Console. В том числе продумать какие у вас будут одноразовые покупки, есть ли нужда в подписках или в многоразовых покупках.
Детально описывать не буду, но дам ссылки на английские версии инструкций. Напишите в комментариях, если нужно разобраться эту тему подробнее в отдельной статье. Найти инструменты для создания всего этого можно в Google Play Console -> «Ваше приложение» -> Страница приложения -> Контент для продажи.
По итогу у вам должен получиться список идентификаторов для всего этого. Например, в своём приложении я используются месячную подписку, годовую и одноразовый платёж для получения премиум-функций, которые не доступны обычным пользователям (у меня будут соответственно следующие идентификаторы monthly, annual и one_time).
Шаги по добавлению Google Play Billing Library в ваш проект
Дальнейшее описание работы с библиотекой я буду переплетать со своим проектом в качестве примера. Как я уже сказал ранее у меня есть базовая версия приложения и премиум, премиум-версию можно получить оформив ежемесячную подписку, годовую (12 месячных с небольшой скидкой) и одноразовый платёж (рассчитывал приблизительно как стоимость 5 годовых подписок).
Задачи, которые мне необходимо решить:
- Подключаться к системе биллинга.
- Получить информацию о встроенных покупках (например, чтобы отображать стоимость на кнопке покупки).
- Возможность оформить подписки/одноразовый платёж.
- Проверять наличие хотя бы одной из озвученных выше позиций в активном состоянии.
Для простоты удобства работы с системой биллинга я вынес её в отдельный класс GooglePlay, который наследует у меня интерфейс, позволяющий выполнять описанные выше задачи. Вы можете сделать по своему и без интерфейса (но он полезен, если у вас есть версия для других магазинов, например, Amazon, у них своя библиотека биллинга).
Ссылка на класс для работы с системой биллинга я храню в производном от класса Application классе приложения (App). Таким образом я могу получить доступ к классу GooglePlay из любой активности, что мне и нужно. Возможно, моё решение не самое оптимальное, но какое есть.
Всё, что вам нужно знать, что лучше выделить отдельный класс для работы с библиотекой и, если вам не нравится оставлять ссылку в классе приложения, то создавайте новый экземпляр этого класса там, где вам надо.
Добавление/обновление gradle зависимостей библиотеки
Первое, что нужно сделать — добавить зависимость в ваш файл build.gradle, которая автоматически будет подтягивать библиотеку со всеми необходимыми классами.
Хочу отметить, что в проектах обычно два файла build.gradle. Один — файл всего проекта, второй — файл настроек для модуля. Если вы опытный разработчик, то сами поймете в какой вам лучше добавлять эту зависимость. Но в стандартном случае нужно добавлять в build.gradle модуля. Определить его можно по названию Module в скобочках.
Вот что нужно добавить, в секцию dependencies:
dependencies {
...
implementation 'com.android.billingclient:billing:2.0.3'
}
После чего нужно принять и обновить изменения в файле, чтобы указанная библиотека загрузилась в проект. На этом шаге могут возникать ошибки конфликта с другими библиотеками, которые решают в отдельном порядке. Если вы не используете проблемные библиотеки из неизвестных источников, то скорее всего всё пройдет гладко.
Задача №1. Создаём подключение к Google Play
Первым делом к нашему классу GooglePlay имплиментируем интерфейс PurchasesUpdatedListener, чтобы потом указывать его в качестве слушателя.
Код, инициализирующий подключение к Google Play я вынес в метод init(Context context) со следующим кодом, вынеся BillingClient billingClient в область видимости всего класса, он нам потребуется:
private BillingClient billingClient;
...
billingClient = BillingClient.newBuilder(context).setListener(this).build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingResponse.OK) {
// The BillingClient is ready. You can query purchases here.
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
});
BillingClient — по сути основной класс, который позволяет выполнять различные методы, его мы и инициализируем, указывая в качестве слушателя GooglePlay (с реализацией интерфейса PurchasesUpdatedListener)
Вызов функции startConnection инициализирует подключение к Google Play, он асинхронный, а значит выполнится не в момент вызова, а скорее всего немного позже, поэтому в качестве параметра указываем ещё одного слушателя, который скажем нам, когда подключение закончилось и закончилось ли оно успешно и когда подключение разорвалось.
Соответственно в функции onBillingSetupFinished вы можете уже начинать какие-то действия по работе с библиотекой. Я, например, если подключение успешное — запускаю функцию, которая проверяет наличие премиум-версии у пользователя или её отсутствия (подробнее об этом далее).
Задача 2. Запрос на получения информации о встроенных покупках.
Вот как выглядит код из инструкции:
List<String> skuList = new ArrayList<> ();
skuList.add("one_time");
skuList.add("one_time_example_2");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
// Process the result.
}
});
Можно выделить это в отдельный метод, передовая только идентификаторы, и например слушателя (чтобы что-то делать с результатом).
Сам код должен быть без проблем вами понят, в списке мы указываем идентификаторы, информация о которых нас интересует. Потом указываем тип покупок, их два SkuType.INAPP или SkuType.SUBS соответственно. В конце вызываем асинхронный метод по запросу информации и когда результаты вернутся будет вызван метод onSkuDetailsResponse, с которым мы уже можем работать.
Допустим у меня есть активность в которой размещены три кнопки для вызова методов покупки, но я хочу прямо на кнопках отображать стоимость, я вызывают метод и передаю в него SkuDetailsResponseListener() объявленный в активности и когда результаты прибудут, задержка на самом деле у меня всегда очень маленькая, то к тексту кнопок я добавляю стоимость в той валюте, которая вернулась. SkuDetails - класс, содержащий в себе информацию о каждой покупке.
Вот вам пример того, как обрабатывать информацию в методе SkuDetailsResponseListener():
if (result.getResponseCode() == BillingResponse.OK && skuDetailsList != null) {
for (SkuDetails skuDetails : skuDetailsList) {
String sku = skuDetails.getSku();
String price = skuDetails.getPrice();
if ("one_time".equals(sku)) {
premiumUpgradePrice = price;
} else if ("monthly".equals(sku)) {
gasPrice = price;
}
}
}
Здесь, в начале проверяется, что запрос прошёл успешно и информация о каких-то покупках вернулась, потом в цикле получается информация о каждой покупке и что-то с ней делается по необходимости.
Важно сохранить эти самые SkuDetails для каждой покупки, если вы хотите отправлять запросы на выполнение покупки. Метод, который это реализует — требует в качестве одного из параметров SkuDetails покупки, а другого метода его получить или создать я не нашёл, хотя может он и есть.
Задача 3. Выполнение запроса на покупку и подтверждение покупки
Теперь можно рассмотреть процесс самой покупки. Первое, что нужно сделать — отправить запрос из библиотеки, выглядит это очень просто:
// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int responseCode = billingClient.launchBillingFlow(flowParams);
Всего две строчки, создаём параметр запроса, указывая там информацию о покупке, которая нас интересует и вызываем функцию показа окна для оплаты.
Как я говорил выше, удобно сохранять информацию (SkuDetails), полученные в задаче 2, чтобы использовать их здесь. Возможно, можно получить их иным способом.
После выполнения этого кода мы увидим окно запроса на оплату.
Чтобы здесь не выбрал пользователь — совершит ли он покупку, отменит её перед самой оплатой или отменит сразу, но будет вызвана функция onPurchasesUpdated интерфейса PurchasesUpdatedListener. В ней и нужно обработать то, прошла ли оплата успешно или нет и предпринять какие-то действия.
Пример обработки функции обновления информации о покупке:
@Override
void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingResponse.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingResponse.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
Важно! Начиная с версии 2.0+ библиотеки помимо просто покупки нужно совершать её подтверждение (Acknowledge a purchase) в течение трех дней с момент совершения покупки, иначе она будет аннулирована, деньги вернутся пользователю и подписка/покупка будет отменена.
Возможно вы уже заметили функцию handlePurchase(purchase); в коде выше, именно её и такой порядок советуют использовать для подтверждения покупки. А вот и код подтверждения:
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = ...
void handlePurchase(Purchase purchase) {
if (purchase.getPurchaseState() == PurchaseState.PURCHASED) {
// Grant entitlement to the user.
...
// Acknowledge the purchase if it hasn't already been acknowledged.
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
client.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
}
Здесь стоит обратить внимание на ещё одного слушателя, который сообщает об вызове функции подтверждения (AcknowledgePurchaseResponseListener).
Есть некоторые различия в подтверждения подписок и расходуемых продуктов в вызове разных методов. В моём случае и для одноразовой покупки и подписок работал этот метод. Подробнее читайте в официальной документации.
Конечно в методе handlePurchase вы можете обрабатывать и другие состояния PurchaseState в зависимости от ваших потребностей.
С момента подтверждения покупки вы вполне успешно получите свои деньги а пользователь должен получить предлагаемые услуги/продукты в вашем приложении, а это подводит меня к последней задаче.
Задача 5. Проверять куплена ли у пользователя премиум-версия приложения
Для хранения информации о премиум-версии я использую обычные SharedPreferences. Проверка наличия премиум-версии происходит после успешного подключения к Google Play (смотри задача 1 функция onBillingSetupFinished). Когда подключение успешно я вызываю функцию, которая проверяет наличие покупок у пользователя следующим кодом:
public void checkPurchase() {
if (mBillingClient.isReady()) {
Purchase.PurchasesResult inapp = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
int inapp_size = 0;
if (inapp != null && inapp.getPurchasesList() != null) inapp_size = inapp.getPurchasesList().size();
int subs_size = 0;
Purchase.PurchasesResult subs = mBillingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (subs != null && subs.getPurchasesList() != null) subs_size = subs.getPurchasesList().size();
app.changePremium(subs_size + inapp_size > 0);
}
}
В моём случае всё довольно просто — я проверяю есть ли хотя бы одна подписка или одноразовая покупка и вызываю функцию changePremium(), которая меняет наличие премиум-версии у пользователя.
Стоит заметить в случае отмены подписки или возврате денег за одноразовую покупку эта функция все равно будет правильно работать, но с некоторым запаздыванием для одноразовой покупки.
То есть, если пользователь захотел схитрить, купить подписку на месяц, потом отменить её — я аннулирую его премиум-версию и забираю все дополнительные функции.
Вы можете использовать проверку подписок по своему и для своих целей, но придется разобраться в библиотеке основательно.
Выводы
На первый взгляд работа с Google Play Billing Library у меня лично вызывала некоторое отторжение и не желание разбираться во всех этих сложных асинхронных вызовах, но на деле оказалось всё довольно понятно и прозрачно, нужно лишь следовать их инструкциями и рекомендациям.
Надеюсь моя статья поможет молодым разработчикам и тем, кто захотел разобраться в библиотеке и подумывает о публикации своего приложения. Я не затронул некоторые вопросы, некоторые не слишком глубоко, пишите в комментариях какие кейсы рассмотреть подробнее и свои вопросы, с удовольствием отвечу.
Не забывайте подписываться на блог, ставьте лайки, делитесь с друзьями, вступайте в мою группу Вконтакте. Своей активностью вы провоцируете меня писать чаще и больше, а это выгодном нам всем ^_^.