Рано или поздно наступает момент, когда разработчику нужно задуматься о том, как монетизировать своё приложение, чтобы оно приносило доход. Есть различные бизнес-модели, с помощью которых можно этого достичь, однако наиболее популярной является использование рекламы в приложении. Одним из плюсов использования рекламы является то, что она хорошо сочетается с другой бизнес-моделью — покупками внутри приложения. Например, пользователь может заплатить некоторую сумму денег для того, чтобы отключить показ рекламы в приложении.
В этой статье мы рассмотрим, как можно реализовать встроенные покупки на примере своего приложения Менеджер паролей от Wi-Fi сетей.
Возможность покупок в приложениях реализована благодаря In-app Billing. In-app Billing — это сервис Google Play, который позволяет продавать цифровой контент внутри приложений. Этот сервис можно использовать для продажи широкого спектра контента, включая загружаемый контент, такой как мультимедийные файлы и фотографии, виртуальный контент, такой как уровни игры или различные вспомогательные предметы, премиальные услуги и многое другое.
Встроенные покупки можно подключить для любого приложения, опубликованного в Google Play. Ничего особенного для этого не требуется, только аккаунт разработчика Google Play Console и аккаунт продавца Google Wallet. Android SDK также содержит пример приложения с реализованными встроенными покупками.
Как работают встроенные покупки?
Ваше приложение обращается к сервису In-app Billing с помощью API, который предоставляется приложением Google Play, установленным на устройстве. Затем Google Play передает платежные запросы и ответы на запросы между вашим приложением и сервером Google Play. Таким образом, ваше приложение никогда напрямую не связывается с сервером Google Play. Вместо этого ваше приложение отправляет запросы в приложение Google Play через межпроцессную связь (IPC) и получает от него ответы, нет необходимости поддерживать какие-либо соединения между вашим приложением и сервером Google Play.
In-app Billing поддерживает широкую совместимость, он работает на устройствах под управлением Android 2.2 (API 8) или выше, на которых установлена последняя версия приложения Google Play.
API In-app Billing предоставляет следующие возможности:
- Ваше приложение отправляет запросы с помощью модернизированного API, который позволяет пользователям легко запрашивать информацию о продукте из Google Play и заказывать продукты в приложении. API быстро восстанавливает продукты на основе прав пользователя.
- API синхронно передает информацию о заказе на устройство при завершении покупки.
- Все покупки регулируемы, т.е. Google Play отслеживает права пользователя на продукты. Пользователь не может владеть несколькими экземплярами одного продукта в приложении; только один экземпляр может принадлежать пользователю в любой момент времени.
- Приобретённые продукты могут быть использованы. В таком случае они возвращаются в бесхозное состояние и могут быть куплены снова.
- API обеспечивает поддержку подписки.
Интеграция In-app Billing в приложение
Есть разные способы, как встроить в своё приложение In-app Billing: можно это делать как вручную, так и используя сторонние библиотеки. Одной из таких библиотек является Checkout, которая уже содержит в себе готовую к применению реализацию сервиса. Ею и воспользуемся.
Checkout — это реализация In-app Billing API. Большим плюсом здесь является, что с помощью этой библиотеки можно сделать интеграцию встроенных покупок в приложение намного проще, чем если бы это делалось вручную с нуля.
Checkout решает общие проблемы, с которыми могут столкнуться разработчики при работе с покупками, например:
- Как отменить все запросы, когда активность уничтожена?
- Как запросить информацию о покупках в фоновом потоке?
- Как проверить покупку?
- Как загрузить все покупки с использованием данных continuationToken или SKU (уникальный идентификатор продукта)?
- Как добавить покупки с минимумом шаблонного кода?
Checkout может быть использован с любым фреймворком или без него. Он имеет четкое разграничение функциональности, доступной в разных контекстах: покупки могут быть сделаны только в активности, тогда как SKU может быть загружен в сервис или класс Application.
Перед началом работы библиотеку нужно добавить в проект. Для этого в файле build.gradle модуля приложения добавить зависимость в блок dependencies.
dependencies { ... compile 'org.solovyev.android:checkout:1.2.1' }
Для работы с покупками требуется специальное разрешение com.android.vending.BILLING, которое будет добавлено в AndroidManifest.xml автоматически с помощью Gradle. Вы также можете добавить его вручную, добавив в файл манифеста следующую строчку перед элементом <application>:
<uses-permission android:name="com.android.vending.BILLING"/>
Создадим экземпляр класса Billing в Application, откуда затем будем брать его при необходимости. Если у вас нет класса Application в проекте, вы можете легко создать его. Для этого нужно добавить в файле AndroidManifest.xml в элемент <application> атрибут android:name=».Имя класса», например:
<application android:name=".App" android:allowBackup="true" android:fullBackupContent="@xml/mybackupscheme" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:resizeableActivity="true" android:roundIcon="@mipmap/ic_launcher_round" android:theme="@style/Theme.DesignDemo">
После этого нужно поставить курсор на имя класса, нажать Alt + Enter и выбрать опцию «Create class», после чего Android Studio создаст его.
В этом классе нам нужно добавить следующий код:
public class App extends Application { public void onCreate() { super.onCreate(); } private final Billing mBilling = new Billing(this, new Billing.DefaultConfiguration() { @Override public String getPublicKey() { return BASE64_PUBLIC_KEY; } }); public Billing getBilling() { return mBilling; } }
BASE64_PUBLIC_KEY это ключ, который используется для установления безопасного подключения между вашим приложением и сервером Google Play. Получить этот ключ вы можете в Google Play Console, перейдя в раздел «Инструменты разработки» — «Службы и API». Там в «Лицензирование и продажа контента» вы увидите сгенерированный для вашего приложения ключ, который нужно будет добавить в приложение, например, объявить как строковую константу в классе Application.
Класс Billing это основной класс для работы с библиотекой, он отвечает за:
- подключение и отключение услуг биллинга;
- выполнение платежных запросов;
- кеширование результатов запросов;
- создание объектов Checkout;
- логирование;
Для того, чтобы избежать множественных подключений к службе In-app Billing, следует использовать только один экземпляр класса Billing, именно по этой причине мы и создаём его в классе Application.
Теперь в классе активности при её создании инициализируем экземпляр класса ActivityCheckout, который наследует от базового класса Checkout.
private ActivityCheckout mCheckout; ... mCheckout = Checkout.forActivity(mainView.getActivity(), App.get().getBilling()); mCheckout.start(); mCheckout.createPurchaseFlow(new PurchaseListener());
Класс Checkout это средний уровень библиотеки, он использует класс Billing в определённом контексте (в Application, активности или сервисе), проверяет, поддерживаются ли покупки на устройстве и выполняет запросы. ActivityCheckout это подкласс, который способен покупать различные предметы, для создания его экземпляра нужно вызвать метод Checkout.forActivity() и передать в параметры активность и экземпляр Billing.
Метод start() запускает созданный экземпляр и отправляет запрос, который проверяет, поддерживается ли биллинг на этом устройстве.
Метод createPurchaseFlow() создаёт постоянный поток для покупок со слушателем, который будет получать обновления данных о покупках. Код слушателя выглядит следующим образом:
private class PurchaseListener extends EmptyRequestListener<Purchase> { @Override public void onSuccess(@Nonnull Purchase purchase) { if (purchase.sku.equals(AD_FREE)) { SP.setBoolean(mainView.getContext(), AD_FREE, true); } if (purchase.sku.equals(DONATE)) { Toast.makeText(mainView.getContext(), R.string.message_donate_tnx, Toast.LENGTH_LONG) .show(); } } }
Класс PurchaseListener наследует от EmptyRequestLisneter<Purchase>, который имеет методы onSuccess() и onError(). В данном случае, если пользователь купит отключение рекламы или сделает пожертвование, то слушатель получит данные о покупке и выполнит нужные операции.
Теперь нужно создать экземпляр класса Invertory.
mCheckout = Checkout.forActivity(mainView.getActivity(), App.get().getBilling()); mCheckout.start(); mCheckout.createPurchaseFlow(new PurchaseListener()); Inventory mInventory = mCheckout.makeInventory(); mInventory.load( Inventory.Request.create().loadAllPurchases().loadSkus(ProductTypes.IN_APP, AD_FREE), new InventoryCallback());
Класс Invertory загружает данные о продуктах, SKU и покупках. Его жизненный цикл связан с жизненным циклом Checkout, в котором он был создан.
Метод makeInvertory() создаёт экземпляр Invertory и привязывает его к нужному объекту Checkout.
Метод load() отправляет запрос на получение данных о продуктах и асинхронно загружает результат в callback. В параметрах формируется запрос, какие именно продукты нужно получить (в данном случае, все имеющиеся, а именно донаты и отключение рекламы). Код коллбека, который принимает результат запроса, представлен ниже:
private class InventoryCallback implements Inventory.Callback { @Override public void onLoaded(@Nonnull Inventory.Products products) { final Inventory.Product product = products.get(ProductTypes.IN_APP); if (!product.supported) { Crashlytics.log(Log.ERROR, "MainPresenterImpl.InventoryCallback", "Billing is not supported, user can't purchase anything"); isBillingSupported = false; return; } List<Purchase> list = product.getPurchases(); if (mainView != null) { if (list.size() == 0) SP.setBoolean(mainView.getContext(), AD_FREE, false); if (product.getSku(AD_FREE) != null) { adFreePrice = product.getSku(AD_FREE).price; } if (product.isPurchased(AD_FREE)) { SP.setBoolean(mainView.getContext(), AD_FREE, true); adRemoved = true; Ads.getInstance().hideBanner(); } } } }
Метод onLoaded() вызывается, когда все данные загружены. В нём проверяются различные данные о продуктах. Например, можно проверить с помощью поля supported можно проверить, поддерживается ли продукт, а метод getSku() возвращает идентификатор продукта. Если нужно узнать стоимость продукта на основе локали устройства, то следует вызывать getSku(TYPE).price.
Метод isPurchased() проверяет, был ли продукт куплен пользователем. В случае с рекламой это будет означать, что её следует отключать.
Теперь нужно отправлять платёжные запросы сервису. Для этого в настройках приложения есть две кнопки «Удалить рекламу» и «Поддержать проект материально».
Обработка кнопки отключения рекламы выглядит следующим образом:
mCheckout.whenReady(new Checkout.EmptyListener() { @Override public void onReady(@NonNull BillingRequests requests) { requests.purchase(ProductTypes.IN_APP, AD_FREE, null, mCheckout.getPurchaseFlow()); Crashlytics.log(Log.INFO, "MainPresenterImpl.removeAds", "Ads was removed"); } });
С помощью данного метода формируется запрос на покупку продукта, результат которого будет получен коллбеком.
Аналогичным образом формируется запрос на донат.
mCheckout.whenReady(new Checkout.EmptyListener() { @Override public void onReady(@NonNull BillingRequests requests) { requests.purchase(ProductTypes.IN_APP, DONATE, null, mCheckout.getPurchaseFlow()); Crashlytics.log(Log.INFO, "MainPresenterImpl.buyDonate", "Got donate"); } });
Таким образом, с помощью библиотеки мы реализовали встроенные покупки в приложении без использования шаблонного кода.
А физ. лицо может зарегистрировать Google Wallet? Или нужно ИП открывать?
Может, в этом плене Google очень демократичная компания. Выплаты начнутся по достижении порога в 100$
А как тестировать покупки? Я прописал в play console тестового пользователя и через него покупаю например рекламу. Деньги не списываются но покупка числиться у него в google play.
Но в классе InventoryCallback mPurchases = 0.
Как правильно тестировать?
Здравствуйте! К сожалению, насчёт тестирования не получится что-либо подсказать, попробуйте написать автору библиотеки на гитхабе.
Про тестирование написано тут в начале статьи https://habrahabr.ru/post/313416/