Покупки в Android приложении — Play Billing Library

image

И как это до сих пор на Хабре нет статьи об этом? Не дело, надо исправлять.

Есть 2 способа добавить In-App покупки в Android-приложение — старый и новый. До 2017 года все пользовались библиотекой от anjlab, но с июня 2017 года ситуация изменилась, Google выпустила собственную библиотеку для внутренних покупок и подписок — Play Billing Library. Сейчас последний считается стандартом.

Play Billing Library это очень просто.

Подключите зависимость.

implementation 'com.android.billingclient:billing:1.2'

Добавьте разрешение в манифесте.

<uses-permission android:name="com.android.vending.BILLING"/>

Создайте инстанс BillingClient и начните соединение.


private BillingClient mBillingClient;
...
mBillingClient = BillingClient.newBuilder(this).setListener(new PurchasesUpdatedListener() {
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {
        if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
            //сюда мы попадем когда будет осуществлена покупка

        }
    }
}).build();
mBillingClient.startConnection(new BillingClientStateListener() {
    @Override
    public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponseCode) {
        if (billingResponseCode == BillingClient.BillingResponse.OK) {
            //здесь мы можем запросить информацию о товарах и покупках

        }
    }

    @Override
    public void onBillingServiceDisconnected() {
        //сюда мы попадем если что-то пойдет не так
    }
});

В метод onPurchasesUpdated() мы попадаем когда покупка осуществлена, в методе onBillingSetupFinished() можно запросить информацию о товарах и покупках.

Запросить информацию о товарах. Поместите querySkuDetails() в onBillingSetupFinished().


private Map<String, SkuDetails> mSkuDetailsMap = new HashMap<>();
private String mSkuId = "sku_id_1";
...
@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponseCode) {
    if (billingResponseCode == BillingClient.BillingResponse.OK) {
        //здесь мы можем запросить информацию о товарах и покупках
        querySkuDetails(); //запрос о товарах

    }
}
...
private void querySkuDetails() {
    SkuDetailsParams.Builder skuDetailsParamsBuilder = SkuDetailsParams.newBuilder();
    List<String> skuList = new ArrayList<>();
    skuList.add(mSkuId);
    skuDetailsParamsBuilder.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
    mBillingClient.querySkuDetailsAsync(skuDetailsParamsBuilder.build(), new SkuDetailsResponseListener() {
        @Override
        public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {
            if (responseCode == 0) {
                for (SkuDetails skuDetails : skuDetailsList) {
                    mSkuDetailsMap.put(skuDetails.getSku(), skuDetails);
                }
            }
        }
    });
}

В коде вы могли заметить понятие SKU, что это? SKU — от английского Stock Keeping Unit (идентификатор товарной позиции).

Теперь в mSkuDetailsMap у нас лежит вся информация о товарах (имя, описание, цена), зарегистрированных в Play Console данного приложения (об этом позже). Обратите внимание на эту строку skuList.add(mSkuId);, здесь мы добавили id товара из Play Console, перечислите здесь все товары, с которыми вы хотите взаимодействовать. У нас товар один —sku_id_1.

Все готово к тому, чтобы выполнить запрос на покупку. Передаем id товара. Запустите этот метод, например, по клику на кнопку.

public void launchBilling(String skuId) {
    BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
            .setSkuDetails(mSkuDetailsMap.get(skuId))
            .build();
    mBillingClient.launchBillingFlow(this, billingFlowParams);
}

Теперь, запустив этот метод, вы увидите вот такое диалоговое окно (прим. картинки из Интернета).

image

Теперь если пользователь купит товар — его ему надо предоставить. Добавьте метод payComplete() и осуществите в нем действия, предоставляющие доступ к купленному товару. Например, если пользователь покупал отключение рекламы, сделайте в этом методе так, чтобы реклама больше не показывалась.

...
@Override
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {
    if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
        //сюда мы попадем когда будет осуществлена покупка
        payComplete();
    }
}
...

Все хорошо, но если пользователь перезапустит приложение, наша программа ничего не знает о покупках. Надо запросить информацию о них. Сделайте это в onBillingSetupFinished().


@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponseCode) {
    if (billingResponseCode == BillingClient.BillingResponse.OK) {
        //здесь мы можем запросить информацию о товарах и покупках
        querySkuDetails(); //запрос о товарах
        List<Purchase> purchasesList = queryPurchases(); //запрос о покупках

        //если товар уже куплен, предоставить его пользователю
        for (int i = 0; i < purchasesList.size(); i++) {
            String purchaseId = purchasesList.get(i).getSku();
            if(TextUtils.equals(mSkuId, purchaseId)) {
                payComplete();
            }
        }
    }
}
...
private List<Purchase> queryPurchases() {
    Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
    return purchasesResult.getPurchasesList();
}

В purchasesList попадает список всех покупок, сделанных пользователем.

Делаем проверку: если товар куплен — выполнить payComplete().

Готово. Осталось это приложение опубликовать в Play Console и добавить товары. Как добавить товар: Описание страницы приложения > Контент для продажи > Создать ограниченный контент.

Примечание 1: Вы не сможете добавить товар пока не загрузите билд приложения в Play Console.

Примечание 2: Чтобы увидеть диалоговое окно о покупке, вам надо загрузить билд в Play Console, добавить товар и подождать какое-то время (~30 минут — 1 час — 3 часа), пока товар обновится, только после этого появится диалоговое окно и можно будет осуществить покупку.

Примечание 3: Ошибка Please fix the input params. SKU can't be null — товар в Play Console еще не успел обновиться, подождите.

Примечание 4: Вы можете столкнуться с ошибкой Error «Your transaction cannot be completed», в логах как response code 6 пока будете тестировать. По каким причинам это происходит мне точно неизвестно, но по моим наблюдениям это происходит после частых манипуляций с покупкой и возвратом товара. Чтобы это починить перейдите в меню банковских карт и передобавьте вашу карту. Как этого избежать? Добавьте ваш аккаунт в Play Console в качестве тестировщика и покупайте только с тестовой карточки.

Демо на GitHub

Купите мне кофе

(Кстати, на Хабре работает система донейтов по кнопке под статьёй — прим. модератора).
Поделиться публикацией

Похожие публикации

Комментарии 23

    +1
    Да, всегда юзал anjlab. Никак не мог понять сэмплы Google Billing'a на GitHub.
    А либа в этой статье и Ваши примеры хороши! Еще не внедрял нигде, но на заметку взял.
    Благодарю за статью!
    +1 в пост и карму!
    Было познавательно! ;-)
      +4
      До 2017 года все пользовались библиотекой от anjlab

      Не все. Я ей не пользовался, а вместо этого просто расковырял документацию, разобрался и сделал все так, как надо.
        0
        Притом что ничего сложного в нативной платежке нет. Зато позволяет понять получше весь биллинг и лучше понимать ошибки, которые могут возникать в процессе.
        Единственная сложность, какая была — получение цены в валюте пользователя и приведение ее к нужному формату.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Ну не знаю что там «нестабильного» — все работало. Я тоже о 2011-2013 — щас на библиотеках, правда на других (unity IAP).

            Вот да, вот эта 1/1000 и подкашивала. Вроде были еще какие то трабблы чтобы получить локализированное название валюты, причем в каких то случаях оно до числа пишется, в каких то после. Но — это все очень глубокое копание, которое я и в этой статье не увидел.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Аа, семпл был да, для истинных спартанцев ) Кто не выжил разобрался — огребали всякое, причем часто уже после выливки — пользователи то массой тестируют. А ведь кейсов там вагон и тележка, да. Веселые времена были )
        +1
        То чувство, когда до сих пор пользуешься anjlab библиотекой, а про офф не слышал) Спасибо за информацию!
          +1
          Добавьте ваш аккаунт в Play Console в качестве тестировщика и покупайте только с тестовой карточки.

          Только нужно не забывать, что учётная запись гугла для тестирования должна отличаться от учётной записи разработка (той которая имеет доступ в google play developer console). В документации я не нашел такого описания, но если погуглить мы найдет не одно описание подобных решений вопросов и ответов (например, тут и https://medium.com/bleeding-edge/testing-in-app-purchases-on-android-a6de74f78878), да и по своему опыту я помню, что именно так оно и работало.

            0

            Так же это указано и в гугловом семпле тут


            Make sure to add your test account (the one you will use to test purchases) to the "testers" section of your app. Your test account CANNOT BE THE SAME AS THE PUBLISHER ACCOUNT. If it is, your purchases won't go through.
              0
              Хорошее замечание, спасибо большое.
              0
              Спасибо за статью! Но еще больше интересно увидеть какой-то пример практической реализации проверки достоверности сделанной покупки с помощью своего сервера (хотя бы PHP). А то при наличии LuckyPatcher и подобного — все эти покупки легко обходятся, насколько я понимаю.

              Я так понимаю, что приложение должно что-то запросить у Гугл API, и запросить свой сервер, чтобы тот что-то запросил у Гугл API. А потом эти два ответа где-то надо сравнить, чтобы убедится в наличии платежа?
                +1
                В «покупке» идёт вместе с данными о покупке и цифровая подпись, которую надо проверить:
                public class Purchase {
                private final String mOriginalJson;
                private final String mSignature;

                Ниже я дал ссылку на пример, там есть функции проверки подписи: github.com/googlesamples/android-play-billing/blob/master/TrivialDriveKotlin/app/src/main/java/com/kotlin/trivialdrive/billingrepo/Security.kt

                Надеюсь, перевести его с Kotlin на PHP не составит труда, там обычная проверка подписи с открытым RSA — ключом.
                  0
                  Благодарю за инфо, но вот как раз практического полного примера PHP и не хватает.
                    0
                    Я не силён в PHP, но вот это оно? php.net/manual/ru/function.openssl-verify.php
                    Приложение передаёт на сервер originalJson и signature, которые используются в openssl_verify()

                    Тут есть ещё одна тонкость, что ответит сервер? Если он на проверку отвечает ok/error, то ничто не помешает в приложении подменить его ответ или убрать эту проверку. В идеале, если приложение после покупки получает доступ к какому-либо контенту, то этот контент должен загружаться с сервера с зашифрованном виде с ключом, который может быть сгенерирован только в этой инсталляции приложения. Если контент слишком большой для загрузки, то сервер может присылать что-то наподобие лицензионного ключа, позволяющего расшифровать контент в приложении.
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        чтобы сервер подписывал ответ

                        Если потом стоит просто if(isSignatureValid) { letsPlay() }
                        То
                        код проверки и валидации, каким бы утончённым он ни был, можно просто заменить на заглушки.

                        Тут в соседней ветке дошли до того, что сервер после валидации покупки должен присылать зашифрованный контент и виртуальную машину для его расшифровывания. ;)

                        можно запрашивать факт покупки заново, и довольно часто).

                        Если у вас хотя бы сотня тысяч клиентов, то придётся поднимать хорошую серверную инфраструктуру для постоянной валидации покупок, а там довольно медленный RSA. Поэтому я и говорил о кэше покупок в приложении, чтобы валидацию делать как можно реже.
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            Так пользователи будут искать приложение в том же Google Play. Конкретный пример: позавчера хабравчане выпустили приложение lamptest.ru. Сейчас в поиске Google Play по запросу lamptest отображается три приложения, а может появиться четвёртое, например, lamptest.pro — перепакованная версия lamptest.ru pro со своей рекламой, доходы от которой потекут в карман хакеру Васе. И пользователи даже не будут разбираться, кто там первый что выпустил. Ведь бесплатная про версия гораздо круче платной про версии ;)
                            • НЛО прилетело и опубликовало эту надпись здесь
                +1
                Не раскрыто две важных темы:
                1. Кэш покупок, чтобы приложение могло работать в оффлайне.
                2. Проверка валидности подписи покупки. Отсюда ещё возникает проблема скрытного хранения ключа проверки этой подписи внутри приложения (что не надёжно) или запуск собственного сервера по проверке этой покупки вне приложения (что рекомендуется Google).
                В общих чертах это разобрано в свежем примере от Google: github.com/googlesamples/android-play-billing/tree/master/TrivialDriveKotlin

                Добавьте разрешение в манифесте.

                Billing-библиотека делает это сама, отдельно не надо ничего добавлять.
                  0
                  Приложение знает о покупках даже после того, как вы очистите данные. Это работает за счет кэша Google Servises, насколько я понимаю.
                    0
                    Да, покупки приходят потом из кэша Play Services, но если у вас валидация покупки происходит на сервере, то как приложение это сделает в оффлайне? Поэтому надо где-то хранить, что такой-то Order ID проверен. И не в открытом виде ;)

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

                Самое читаемое