Найти тему
IT-предприниматель

Google Play Billing Library. Разбор новой библиотеки 2.0.0+

Оглавление

Доброго времени суток, уважаемый читатель. В статье я хочу рассказать о том, как сделать встроенные покупки и подписки при помощи новой версии библиотеки для биллинга от Google. Статья является гибридом перевода официальной документации с моими комментариями (Официальная документация на английском).

Все примеры будут рассматриваться на языке Java (я работаю именно на нем, не Kotlin).

Преподготовка

Перед тем как приступать к непосредственному написанию кода необходимо подготовить всё в вашей Google Play Console. В том числе продумать какие у вас будут одноразовые покупки, есть ли нужда в подписках или в многоразовых покупках.

Детально описывать не буду, но дам ссылки на английские версии инструкций. Напишите в комментариях, если нужно разобраться эту тему подробнее в отдельной статье. Найти инструменты для создания всего этого можно в Google Play Console -> «Ваше приложение» -> Страница приложения -> Контент для продажи.

По итогу у вам должен получиться список идентификаторов для всего этого. Например, в своём приложении я используются месячную подписку, годовую и одноразовый платёж для получения премиум-функций, которые не доступны обычным пользователям (у меня будут соответственно следующие идентификаторы monthly, annual и one_time).

Шаги по добавлению Google Play Billing Library в ваш проект

Дальнейшее описание работы с библиотекой я буду переплетать со своим проектом в качестве примера. Как я уже сказал ранее у меня есть базовая версия приложения и премиум, премиум-версию можно получить оформив ежемесячную подписку, годовую (12 месячных с небольшой скидкой) и одноразовый платёж (рассчитывал приблизительно как стоимость 5 годовых подписок).

Задачи, которые мне необходимо решить:

  1. Подключаться к системе биллинга.
  2. Получить информацию о встроенных покупках (например, чтобы отображать стоимость на кнопке покупки).
  3. Возможность оформить подписки/одноразовый платёж.
  4. Проверять наличие хотя бы одной из озвученных выше позиций в активном состоянии.

Для простоты удобства работы с системой биллинга я вынес её в отдельный класс GooglePlay, который наследует у меня интерфейс, позволяющий выполнять описанные выше задачи. Вы можете сделать по своему и без интерфейса (но он полезен, если у вас есть версия для других магазинов, например, Amazon, у них своя библиотека биллинга).

Ссылка на класс для работы с системой биллинга я храню в производном от класса Application классе приложения (App). Таким образом я могу получить доступ к классу GooglePlay из любой активности, что мне и нужно. Возможно, моё решение не самое оптимальное, но какое есть.

Всё, что вам нужно знать, что лучше выделить отдельный класс для работы с библиотекой и, если вам не нравится оставлять ссылку в классе приложения, то создавайте новый экземпляр этого класса там, где вам надо.

Добавление/обновление gradle зависимостей библиотеки

Первое, что нужно сделать — добавить зависимость в ваш файл build.gradle, которая автоматически будет подтягивать библиотеку со всеми необходимыми классами.

Хочу отметить, что в проектах обычно два файла build.gradle. Один — файл всего проекта, второй — файл настроек для модуля. Если вы опытный разработчик, то сами поймете в какой вам лучше добавлять эту зависимость. Но в стандартном случае нужно добавлять в build.gradle модуля. Определить его можно по названию Module в скобочках.
build.gradle модуля
build.gradle модуля

Вот что нужно добавить, в секцию 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 у меня лично вызывала некоторое отторжение и не желание разбираться во всех этих сложных асинхронных вызовах, но на деле оказалось всё довольно понятно и прозрачно, нужно лишь следовать их инструкциями и рекомендациям.

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

Не забывайте подписываться на блог, ставьте лайки, делитесь с друзьями, вступайте в мою группу Вконтакте. Своей активностью вы провоцируете меня писать чаще и больше, а это выгодном нам всем ^_^.