В этой статье мы бы хотели поделиться своим опытом интеграции библиотеки OpenIAB в наше Android приложение по заучиванию английских слов: ссылка (iOS и Android). Если кто не знает, то OpenIAB это библиотека, которая позволяет подключать in-App покупки различных магазинов приложений, абстрагируясь от деталей реализации API конкретного магазина.
OpenIAB разрабатывается исходя из следующий принципов:
* API библиотеки должно быть макисмально похоже на API Google Play In-app Billing.
* Один APK файл должен работать для всех поддерживаемых магазинах приложени.
* Никаких посрединков при проведении оплат. Это значит что нет никаких третьих сторон которые обрабаытвают транцакии. Под капотом библиотеки все транцакии обрабатываются все теми же Google Play, Yandex.Store и другими нативными приложениями магазинов. По сути, OpenIAB является прослойкой, приводящей API различных аппсторов к одному API, который мы и будем использовать в своем приложении.
Процесс подключения
Подключение библиотеки OpenIAB к проекту
Чтобы воспользоваться API библиотеки, нам, конечно же, нужно подключить ее к нашему проекту. Сделать это можно несколькими способами:
1. Скачать jar файл библиотеки отсюда(http://www.onepf.org/openiab) и положить его в папку lib нашего eclipse проекта.
2. Клонировать проект из git репозитория (git clone github.com/onepf/OpenIAB.git) и подключить его к проекту как Library Project.
Первый вариант является более простым, им и воспользуемся.
Для того чтобы библиотека могла взаимодействовать с магазинами приложений, ей понадобится подключение к интернету, поэтому не забываем добавить разрешение <uses-permission android:name=«android.permission.INTERNET»/> в AndroidManifest.xml.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
...
<!-- permissions to download files and billing support -->
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
На этом подключение библиотеки к проекту закончено, и пришло время настроить ее для работы с магазином приложений.
Настройка
Наше приложение распространяется через Yandex.Store, и настройку библиотеки мы будем рассматривать на примере именно этого магазина. Для начала нужно создать то, что мы хотим продавать. Для этого нужно проследовать в Developer Console (https://developer.store.yandex.ru) Яндекса и создать там новое приложение. Далее в разделе «Покупки в приложении» нужно создать запись для каждой позиции виртуального товара, который мы будем предлагать пользователю. При добавлении каждой покупки среди прочих параметров от нас потребуется указать ее ID. Эти ID мы будем использовать при взаимодействии с магазином приложений.
Теперь, когда покупки существуют на стороне магазина, пришло время подготовить наше приложение к работе с Yandex.Store. Нагляднее всего будет сразу показать код.
public class DictActivity extends Activity
{
//Объект для доступа к API библиотеки.
private OpenIabHelper mIABHelper;
private boolean isIABHelperSetup = false;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dict);
//Указываем настройки API.
OpenIabHelper.Options.Builder builder = new OpenIabHelper.Options.Builder()
.setCheckInventory(true)
.addPreferredStoreName(OpenIabHelper.NAME_YANDEX)
.setVerifyMode(OpenIabHelper.Options.VERIFY_EVERYTHING)
.addStoreKeys(SettingsManager.STORE_KEYS_MAP);
//Собственно инициализация библиотеки.
mIABHelper = new OpenIabHelper(this, builder.build());
mIABHelper.startSetup(new IabHelper.OnIabSetupFinishedListener()
{
public void onIabSetupFinished(IabResult result)
{
if(!result.isSuccess())
{
isIABHelperSetup = false;
//TODO: самое время сообщить пользователю о проблеме.
return;
}
if(mIABHelper != null)
{
isIABHelperSetup = true;
//API готово к использованию.
}
}
});
}
@Override
public void onDestroy()
{
super.onDestroy();
if(mIABHelper != null)
mIABHelper.dispose();
mIABHelper = null;
}
}
mIABHelper это тот самый объект, через который будет осуществляться доступ к API библиотеки. Его настройка происходит посредством передачи в конструктор структуры OpenIabHelper.Options. Давайте рассмотрим наиболее полезные из возможных настроек:
— setCheckInventory(boolean checkInventory). Говорит библиотеке OpenIAB подключаться к каждому доступному магазину приложений, получать из него список доступных пользователю покупок и на основе полученных данных определять подходит ли данный магазин для дальнейшей работы или нет.
— addPreferredStoreName(String… storeNames). Задает приоритет использования доступных магазинов. Может быть полезна, если покупка может быть проведена через несколько магазинов одновременно.
— addStoreKeys(Map<String, String> storeKeys). Многие магазины приложений, в том числе и Yandex.Store, подписывают отсылаемые приложению сообщения. Таким образом, проверив подпись, приложение всегда может удостовериться в том, что сообщение не поддельное. Для проверки подписи используется публичный ключ, который, как правило, можно получить из консоли разработчика конкретного магазина приложений. С помощью данной опции можно задать публичные ключи для используемых магазинов.
-setVerifyMode(int verifyMode). Определяет режим проверки подписи при анализе сообщений магазина. Всего доступно три режима:
* OpenIabHelper.Options.VERIFY_EVERYTHING — всегда проверять подпись. Если для магазина не указан публичный ключ, то работа с ним будет не возможна.
* OpenIabHelper.Options.VERIFY_SKIP — не проверять подпись. Может быть полезно, если вы обрабатываете покукпки на сервере, а не в приложении.
* OpenIabHelper.Options.VERIFY_ONLY_KNOWN — проверять подпись, только если для данного магазина указан публичный ключ, например опцией addStoreKeys(Map<String, String> storeKeys).
Метод OpenIabHelper.startSetup(IabHelper.OnIabSetupFinishedListener) запускает процесс инициализации библиотеки. Это асинхронный вызов, и результат операции будет возвращен в метод onIabSetupFinished() переданного объекта.
Чтобы наше приложение могло использовать Yandex.Store для осуществления покупок, нам нужно дать ему соответствующие разрешение в файле манифеста проекта.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
...
<!-- permissions to download files and billing support -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="org.onepf.openiab.permission.BILLING" />
...
</manifest>
Каркас готов, мы почти готовы что-нибудь продать.
Обработка покупок
Для того чтобы начать процесс покупки, нам необходимо сделать вызов
OpenIabHelper.launchPurchaseFlow(Activity act, String sku, int requestCode, IabHelper.OnIabPurchaseFinishedListener listener).
...
if((mIABHelper != null) && isIABHelperSetup)
{
String sku = "duct_1";
mIABHelper.launchPurchaseFlow(DictActivity.this, sku, 10001, mPurchaseFinishedListener);
}
...
Здесь
act — активити, из которой осуществляется покупка.
sku — тот самый ID, который мы указывали при регистрации покупки в магазине приложений.
requestCode — код вызова активити магазина.
listener — слушатель, в котором мы будем обрабатывать результат операции.
Сама по себе библиотека OpenIAB не делает никаких запросов, а лишь делегирует вызовы к приложению выбранного магазина. В нашем случае таким приложением будет Yandex.Store. Таким образом после вызова launchPurchaseFlow() пользователь будет перенаправлен в приложение соответствующего магазина, которое и будет проводить транзакцию покупки. После того как пользователь подтвердит или не подтвердит покупку, мы получим результат в методе onActivityResult() нашего активити. Для того чтобы библиотека OpenIAB могла интерпретировать полученный ответ, мы должны ей этот ответ передать. Делается это вызовом handleActivityResult(int requestCode, int resultCode, Intent data). Этот метод интерпретирует полученные данные от приложения магазина, проверяет подпись и передает управление в метод onIabPurchaseFinished(IabResult result, Purchase purchase) указанного при вызове launchPurchaseFlow() слушателя.
public class DictActivity extends Activity
{
...
private void makePurchase()
{
if((mIABHelper != null) && isIABHelperSetup)
{
String sku = "duct_1";
mIABHelper.launchPurchaseFlow(DictActivity.this, sku, 10001, mPurchaseFinishedListener);
}
}
...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
if((mIABHelper == null) || !mIABHelper.handleActivityResult(requestCode, resultCode, data))
super.onActivityResult(requestCode, resultCode, data);
}
...
private OnIabPurchaseFinishedListener mPurchaseFinishedListener = new OnIabPurchaseFinishedListener()
{
public void onIabPurchaseFinished(IabResult result, Purchase purchase)
{
if(result.isFailure())
{
//TODO: что-то пошло не так, сообщаем пользователю о проблеме.
return;
}
//Пользователь осуществил покупку
String purchaseName = purchase.getSku();
markPurchaseAsConfirmed(purchaseName);
}
};
private void markPurchaseAsConfirmed(String purchase)
{
//TODO: сохраняем информацию о том что товар куплен и делаем соответствующие
//изменения в UI приложения.
}
}
Теперь у пользователя есть возможность стать счастливым обладателем нашего виртуального товара, и пришло время внести последний штрих в наше приложение, а именно…
Восстановление состояния покупок
Обычно магазины приложений поддерживают несколько типов товаров. Наиболее распространенные типы это: потребляемые товары, не потребляемые товары и подписки. К потребляемым товарам можно отнести, наример, внутриигровую валюту. Пользователь может осуществлять покупку потребляемого товара несколько раз. Примером не потребляемого товара может служить какая-либо дополнительная функциональность вашего приложения. Пользователь оплачивает ее один раз и пользуется всегда.
В нашем случае мы даем возможность пользователю докупать дополнительные словари, которые, по логике, должны быть не потребляемым товаром.Особенность не потребляемых товаров в том, что они привязываются к учетной записи пользователя, а не к конкретному экземпляру программы. Это означает, что даже если пользователь установил ваше приложение на несколько устройств под одной учетной записью, покупку товара он может осуществить только один раз, и купленный товар должен быть доступен на всех этих устройствах. Поэтому мы должны иметь возможность получать из магазина приложений информацию об уже купленных когда-то товарах, чтобы пользователь мог ими воспользоваться во вновь установленном приложении. Кроме того, процесс обработки транзакции покупки достаточно сложен и связан с передачей данных по сети и взаимодействием нескольких приложений внутри самой системы. В любой момент что-то может пойти не так. В худшем случае сбой может произойти после того как покупка была осуществлена с точки зрения магазина приложений, но до того как мы обработали покупку в нашем приложении. В этом случае пользователь не получит желаемый товар в приложении, хотя он будет числиться как купленный в магазине приложений. Для обработки таких ситуаций предусмотрен вызов
queryInventoryAsync(IabHelper.QueryInventoryFinishedListener listener).
Данный метод сделает асинхронный запрос к магазину приложений на предмет наличия купленных пользователем товаров и вернет ответ слушателю listener. Неплохим решением будет вызвать данный метод сразу после того, как библиотека OpenIAB была проинициализирована.
public class DictActivity extends Activity
{
...
@Override
public void onIabSetupFinished(IabResult result)
{
if(!result.isSuccess())
{
isIABHelperSetup = false;
//TODO: самое время сообщить пользователю о проблеме.
return;
}
if(mIABHelper != null)
{
isIABHelperSetup = true;
//API готово к использованию.
//Запрашиваем информацию по купленным товарам.
mIABHelper.queryInventoryAsync(mGotInventoryListener);
}
}
...
private QueryInventoryFinishedListener mGotInventoryListener = new QueryInventoryFinishedListener()
{
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inventory)
{
if (result.isFailure())
{
//TODO: сообщаем пользователю ошибку.
return;
}
// Проверяем есть ли купленные и не установленные словари.
List<Purchase> purchases = inventory.getAllPurchases();
for(Purchase p : purchases)
{
String sku = p.getSku();
if(!isPurchaseConfirmed(sku))
markPurchaseAsConfirmed(sku);
}
}
};
private boolean isPurchaseConfirmed(String purchase)
{
//TODO: возвращаем true, если товар уже отмечен как купленный,
//иначе возвращаем false.
}
private void markPurchaseAsConfirmed(String purchase)
{
//TODO: сохраняем информацию о том, что товар куплен, и делаем соответствующие
//изменения в UI приложения.
}
}
Все вместе
public class DictActivity extends BaseActivity
{
//Объект для доступа к API библиотеки.
private OpenIabHelper mIABHelper;
private boolean isIABHelperSetup = false;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dict);
//Указываем настройки API.
OpenIabHelper.Options.Builder builder = new OpenIabHelper.Options.Builder()
.setCheckInventory(true)
.addPreferredStoreName(OpenIabHelper.NAME_YANDEX)
.setVerifyMode(OpenIabHelper.Options.VERIFY_EVERYTHING)
.addStoreKeys(SettingsManager.STORE_KEYS_MAP);
//Собственно инициализация библиотеки.
mIABHelper = new OpenIabHelper(this, builder.build());
mIABHelper.startSetup(new IabHelper.OnIabSetupFinishedListener()
{
public void onIabSetupFinished(IabResult result)
{
if(!result.isSuccess())
{
isIABHelperSetup = false;
//TODO: самое время сообщить пользователю о проблеме.
return;
}
if(mIABHelper != null)
{
isIABHelperSetup = true;
//API готово к использованию.
//Запрашиваем информацию по купленным товарам.
mIABHelper.queryInventoryAsync(mGotInventoryListener);
}
}
});
final Button buy = (Button)findViewById(R.id.buy);
buy.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
if((mIABHelper != null) && isIABHelperSetup)
{
String payload = "payload";
String sku = "duct_1";
mIABHelper.launchPurchaseFlow(DictActivity.this, sku, 10001, mPurchaseFinishedListener, payload);
}
}
});
}
@Override
public void onDestroy()
{
super.onDestroy();
if(mIABHelper != null)
mIABHelper.dispose();
mIABHelper = null;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
if((mIABHelper == null) || !mIABHelper.handleActivityResult(requestCode, resultCode, data))
super.onActivityResult(requestCode, resultCode, data);
}
private OnIabPurchaseFinishedListener mPurchaseFinishedListener = new OnIabPurchaseFinishedListener()
{
public void onIabPurchaseFinished(IabResult result, Purchase purchase)
{
if (result.isFailure())
{
showMessage("Error purchasing: " + result);
return;
}
String purchaseName = purchase.getSku();
markPurchaseAsConfirmed(purchaseName);
}
};
private QueryInventoryFinishedListener mGotInventoryListener = new QueryInventoryFinishedListener()
{
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inventory)
{
if (result.isFailure())
{
//TODO: сообщаем пользователю ошибку.
return;
}
// Проверяем есть ли купленные и не установленные словари.
List<Purchase> purchases = inventory.getAllPurchases();
for(Purchase p : purchases)
{
String sku = p.getSku();
if(!isPurchaseConfirmed(sku))
markPurchaseAsConfirmed(sku);
}
}
};
private void markPurchaseAsConfirmed(String purchase)
{
//TODO: сохраняем информацию о том, что товар куплен, и делаем соответствующие
//изменения в UI приложения.
}
private boolean isPurchaseConfirmed(String purchase)
{
//TODO: возвращаем true, если покупка уже отмечена как купленная,
//иначе возвращаем false.
}
}
Вывод
Если вы планируете зарабатывать на своем приложении за пределами инфраструктуры google play, то OpenIAB это то, что вам нужно. Подключение новых магазинов приложений осуществляется буквально несколькими строками кода, а схожесть API с Google Billing API позволит быстро мигрировать на нее с Google Play.
Ссылки
github.com/onepf/OpenIAB/ — проект на GitHub.
www.engwords.net/ru — для примера, сайт приложения со встроенным OpenIAB для Yandex.Store
Автор: LeoRed