Перейти к основному содержимому

3.2.0

RuStore позволяет интегрировать платежи в мобильное приложение.

Пример реализации

Ознакомьтесь с приложением-примером чтобы узнать, как правильно интегрировать SDK отзывов и оценок.

Условия работы платежей

  • На устройстве пользователя установлена актуальная версия RuStore.
  • Пользователь авторизован в RuStore.
  • Пользователь и приложение не должны быть заблокированы в RuStore.
  • Для приложения включена возможность покупок в Консоли RuStore.
предупреждение

Сервис имеет некоторые ограничения на работу за пределами России.

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

Добавление репозитория

Подключите репозиторий, как показано в примере ниже.

build.gradle
repositories {
maven {
url = uri("https://artifactory-external.vkpartner.ru/artifactory/maven")
}
}

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

Добавьте следующий код в свой конфигурационный файл для подключения зависимости.

build.gradle
dependencies {
implementation("ru.rustore.sdk:billingclient:3.2.0")
}

Для корректной работы оплаты через сторонние приложения (СБП, SberPay и др.) необходимо правильно реализовать обработку deeplink. Укажите в AndroidManifest.xml intent-filter с scheme вашего проекта (см. ниже).

AndroidManifest.xml
<activity
android:name=".YourBillingActivity">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourappscheme" />
</intent-filter>

</activity>

Здесь: yourappscheme — схема вашего deeplink, её можно изменить на другую.

Эта схема должна совпадать со схемой deeplink, указываемой при инициализации библиотеки billing-клиента.

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

class YourBillingActivity: AppCompatActivity() {

// Previously created with RuStoreBillingClientFactory.create()
private val billingClient: RuStoreBillingClient = YourDependencyInjection.getBillingClient()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
billingClient.onNewIntent(intent)
}
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
billingClient.onNewIntent(intent)
}
}

Для восстановления состояния вашего приложения при возврате с deeplink добавьте в AndroidManifest.xml android:launchMode="singleTask".

AndroidManifest.xml
<activity
android:name=".YourBillingActivity"
android:launchMode="singleTask"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">

Инициализация

Перед вызовом методов библиотеки необходимо выполнить её инициализацию.

Создайте RuStoreBillingClient, используя RuStoreBillingClientFactory.create().

val billingClient: RuStoreBillingClient = RuStoreBillingClientFactory.create(
context = app,
consoleApplicationId = "111111",
deeplinkScheme = "yourappscheme",
// Опциональные параметры
themeProvider = null,
debugLogs = false,
externalPaymentLoggerFactory = null,
)
  • context — контекст Android. Может быть любым, в реализации используется applicationContext.
  • consoleApplicationId — код приложения из консоли разработчика RuStore (пример: https://console.rustore.ru/apps/123456).
примечание

ApplicationId, указанный в build.gradle, должен совпадать с applicationId APK-файла, который вы публиковали в Консоли RuStore.

  • deeplinkScheme — схема deeplink, необходимая для возврата в ваше приложение после оплаты через стороннее приложение (например, SberPay или СБП). SDK генерирует свой хост к данной схеме.
примечание
Схема deeplink, передаваемая в yourappscheme, должна совпадать со схемой, указанной в AndroidManifest.xml (подробнее см. Обработка deeplink).
  • themeProvider — интерфейс, который предоставляет тему BillingClientTheme. Возможны 2 реализации темы BillingClientTheme: светлая (Light) и тёмная (Dark).Необязательный интерфейс, по умолчанию применяться светлая тема.
  • externalPaymentLoggerFactory — интерфейс, который предоставляет доступ ко внешней системе ведения журнала событий.
  • debugLogs — флаг регулирующий ведение журнала событий (логи будут автоматически отключены для Release-сборок).
примечание

Подпись keystore должна совпадать с подписью, которой было подписано приложение, опубликованное в Консоли RuStore. Убедитесь, что используемый buildType (пр. debug) использует такую же подпись, что и опубликованное приложение (пр. release).

Как работают платежи

Проверка доступности работы с платежами

Во время проверки доступности платежей проверяются следующие условия.

  • На устройстве пользователя установлена актуальная версия RuStore.
  • Приложение RuStore поддерживает функциональность платежей.
  • Пользователь авторизован в RuStore.
  • Пользователь и приложение не должны быть заблокированы в RuStore.
  • Для приложения включена возможность покупок в Консоли RuStore.
Для проверки доступности платежей используйте метод checkPurchasesAvailability. Если все указанные выше условия выполняются, возвращается FeatureAvailabilityResult.Available. В противном случае возвращается FeatureAvailabilityResult.Unavailable(val cause: RuStoreException), где cause — это ошибка о невыполненном условии.

Все возможные ошибки RuStoreException описаны в разделе Обработка ошибок. Прочие ошибки возвращаются в onFailure.

RuStoreBillingClient.checkPurchasesAvailability(context)
.addOnSuccessListener { result ->
when (result) {
FeatureAvailabilityResult.Available -> {
// Process purchases available
}

is FeatureAvailabilityResult.Unavailable -> {
// Process purchases unavailable
}
}
}.addOnFailureListener { throwable ->
// Process unknown error
}

context — контекст Android.

Работа с SDK

Получение списка продуктов

Для получения списка продуктов используйте метод getProducts.
val productsUseCase: ProductsUseCase = billingClient.products
productsUseCase.getProducts(productIds = listOf("id1", "id2"))
.addOnSuccessListener { products: List<Product> ->
// Process success
}
.addOnFailureListener { throwable: Throwable ->
// Process error
}

productIds — список идентификаторов продуктов. Максимальная длина — 2083 символа в списке.

Метод возвращает

data class Product(
val productId: String,
val productType: ProductType?,
val productStatus: ProductStatus,
val priceLabel: String?,
val price: Int?,
val currency: String?,
val language: String?,
val title: String?,
val description: String?,
val imageUrl: Uri?,
val promoImageUrl: Uri?,
val subscription: ProductSubscription?,
)

Структура продукта

  • productId — идентификатор продукта.
  • productType — тип продукта.
  • productStatus — статус продукта.
  • priceLable — отформатированная цена товара, включая валютный знак на языке language.
  • price — цена в минимальных единицах (в копейках).
  • currency — код валюты ISO 4217.
  • language — язык, указанный с помощью BCP 47 кодирования.
  • title — название продукта на языке language.
  • description — описание на языке language.
  • imageUrl — ссылка на картинку.
  • promoImageUrl — ссылка на промокартинку.
  • subscription — описание подписки, возвращается только для продуктов с типом subscription.

Структура подписки

data class ProductSubscription(
val subscriptionPeriod: SubscriptionPeriod?,
val freeTrialPeriod: SubscriptionPeriod?,
val gracePeriod: SubscriptionPeriod?,
val introductoryPrice: String?,
val introductoryPriceAmount: String?,
val introductoryPricePeriod: SubscriptionPeriod?
)
  • subscriptionPeriod — период подписки.
  • freeTrialPeriod — пробный период подписки.
  • gracePeriod — льготный период подписки.
  • introductoryPrice — отформатированная вступительная цена подписки, включая знак валюты, на языке product:language.
  • introductoryPriceAmount — вступительная цена в минимальных единицах валюты (в копейках).
  • introductoryPricePeriod — расчётный период вступительной цены.

Структура периода подписки

data class SubscriptionPeriod(
val years: Int,
val months: Int,
val days: Int,
)
  • years — количество лет.
  • months — количество месяцев.
  • days — количество дней.

Покупка продукта

Для вызова покупки продукта используйте метод purchaseProduct.

val purchasesUseCase: PurchasesUseCase = billingClient.purchases
purchasesUseCase.purchaseProduct(
productId = productId,
orderId = UUID.randomUUID().toString(),
quantity = 1,
developerPayload = null,
).addOnSuccessListener { paymentResult: PaymentResult ->
when (paymentResult) {
// Process PaymentResult
}
}.addOnFailureListener { throwable: Throwable ->
// Process error
}
  • productId: String — идентификатор продукта.
  • orderId: String — уникальный идентификатор оплаты, сформированный приложением (UUID).
  • quantity: Int — количество продукта.
  • developerPayload — указанная разработчиком строка, содержащая дополнительную информацию о заказе.

Структура результата покупки

public sealed interface PaymentResult {     

public data class Success(
val orderId: String?,
val purchaseId: String,
val productId: String,
val invoiceId: String,
val subscriptionToken: String? = null,
) : PaymentResult

public data class Cancelled(
val purchaseId: String,
) : PaymentResult

public data class Failure(
val purchaseId: String?,
val invoiceId: String?,
val orderId: String?,
val quantity: Int?,
val productId: String?,
val errorCode: Int?,
) : PaymentResult

public object InvalidPaymentState : PaymentResult()
}
  • Success - результат успешного завершения покупки цифрового товара.
  • Failure - при отправке запроса на оплату или получения статуса оплаты возникла проблема, невозможно установить статус покупки.
  • Cancelled — запрос на покупку отправлен, при этом пользователь закрыл «платёжную шторку» на своём устройстве, и результат оплаты неизвестен.
  • InvalidPaymentState — ошибка работы SDK платежей. Может возникнуть, в случае некорректного обратного deeplink.

Получение сведений о покупке

Для получения информации о покупке, используйте метод getPurchaseInfo.
val purchasesUseCase: PurchasesUseCase = billingClient.purchases
purchasesUseCase.getPurchaseInfo("purchaseId")
.addOnSuccessListener { purchase: Purchase ->
// Process success
}
.addOnFailureListener { throwable: Throwable ->
// Process error
}

Структура покупки

data class Purchase(
val purchaseId: String?,
val productId: String,
val productType: ProductType?,
val invoiceId: String?,
val description: String?,
val language: String?,
val purchaseTime: Date?,
val orderId: String?,
val amountLabel: String?,
val amount: Int?,
val currency: String?,
val quantity: Int?,
val purchaseState: PurchaseState?,
val developerPayload: String?,
val subscriptionToken: String?
)
  • purchaseId — идентификатор покупки.
  • productId — идентификатор продукта.
  • description — описание на языке language.
  • invoiceId — идентификатор счёта.
  • language — язык, указанный с помощью BCP 47 кодирования.
  • purchaseTime — время покупки.
  • orderId — уникальный идентификатор оплаты, сформированный приложением (UUID).
  • amountLable — отформатированная цена покупки, включая валютный знак на языке language.
  • amount — цена в минимальных единицах валюты.
  • currency — код валюты ISO 4217.
  • quantity — количество продукта.
  • purchaseState — состояние покупки:
    • CREATED — покупка создана;
    • INVOICE_CREATED — по покупке создан счёт, ожидает оплаты;
    • PAID — только покупки потребляемого товара — промежуточный статус, средства на счёте покупателя зарезервированы. Покупка ожидает подтверждения от разработчика;
    • CONFIRMED — финальный статус, покупка подтверждена (для подписок и непотребляемых товаров). Средства отправлены разработчику. Повторная покупка товара блокируется магазином;
    • CONSUMED — для потребляемых товаров — конечный статус, потребление покупки подтверждено. Можно производить повторную покупку товара;
    • CANCELLED — покупка отменена — оплата не была произведена или был совершен возврат средств покупателю (для подписок после возврата средств покупка не переходит в CANCELLED);
    • CLOSED — для подписок — подписка перешла в HOLD период или закрылась.
  • developerPayload — указанная разработчиком строка, содержащая дополнительную информацию о заказе.
  • subscriptionToken — токен для валидации покупки на сервере.

Статусная модель (purchaseState)

Статусная модель покупки потребляемых продуктов (CONSUMABLES)

img

Статусная модель покупки непотребляемых продуктов (NON-CONSUMABLES)

img

Статусная модель покупки подписок (SUBSCRIPTIONS)

img

Получение списка покупок

Метод возвращает только покупки со статусами из таблицы ниже. Подробнее о других возможных состояниях покупки смотрите в разделе Получение сведений о покупке.

Тип/СтатусINVOICE_CREATEDCONFIRMEDPAID
CONSUMABLE++
NON-CONSUMABLE++
SUBSCRIPTION++
примечание

Метод возвращает незавершённые состояния покупки и покупки потребляемых товаров, требующих обработки. Помимо этого, он показывает подтверждённые покупки для подписок и непотребляемых товаров — тех, которые нельзя купить повторно.

Для получения списка покупок пользователя используйте метод getPurchases.

val purchasesUseCase: PurchasesUseCase = billingClient.purchases
purchasesUseCase.getPurchases()
.addOnSuccessListener { purchases: List<Purchase> ->
// Process success
}
.addOnFailureListener { throwable: Throwable ->
// Process error
}

Структура покупки

data class Purchase(
val purchaseId: String?,
val productId: String,
val productType: ProductType?,
val invoiceId: String?,
val description: String?,
val language: String?,
val purchaseTime: Date?,
val orderId: String?,
val amountLabel: String?,
val amount: Int?,
val currency: String?,
val quantity: Int?,
val purchaseState: PurchaseState?,
val developerPayload: String?,
val subscriptionToken: String?
)
  • purchaseId — идентификатор покупки.
  • productId — идентификатор продукта.
  • productType — тип продукта.
  • invoiceId — идентификатор счёта.
  • description — описание на языке language.
  • language — язык, указанный с помощью BCP 47 кодирования.
  • purchaseTime — время покупки.
  • orderId — уникальный идентификатор оплаты, сформированный приложением (UUID).
  • amountLable — отформатированная цена покупки, включая валютный знак на языке language.
  • amount — цена в минимальных единицах валюты.
  • currency — код валюты ISO 4217.
  • quantity — количество продукта.
  • purchaseState — состояние покупки.

Структура ответа сервера на запрос покупок

data class PurchasesResponse(
override val meta: RequestMeta?,
override val code: Int,
override val errorMessage: String?,
override val errorDescription: String?,
override val errors: List<DigitalShopGeneralError>?,
val purchases: List<Purchase>?,
) : ResponseWithCode
  • meta — дополнительная информация о запросе.
  • code — код ответа.
  • errorMessage — сообщение об ошибке для пользователя.
  • errorDescription — расшифровка сообщения об ошибке.
  • errors — список ошибок для запрошенных продуктов.
  • purchases — список запрошенных покупок.

Валидация покупки на сервере

Если вам необходимо произвести валидацию успешной покупки на сервере методами API RuStore, вы можете использовать subscriptionToken в PurchaseResult, возвращаемой purchaseProduct при успешной покупке продукта.

SubscriptionToken состоит из invoiceId покупки и userId, записанных через точку: $invoiceId.$userId.

Также можно получить subscriptionToken в сущности Purchase. Сущность Purchase можно получить используя метод getPurchases.

val purchasesUseCase: PurchasesUseCase = billingClient.purchases
purchasesUseCase.purchaseProduct(productId).addOnSuccessListener { paymentResult ->
if (paymentResult is PaymentResult.Success) {
val subscriptionToken = paymentResult.subscriptionToken
yourApi.validate(subscriptionToken)
}
}

Также можно получить subscriptionToken в сущности Purchase. Сущность Purchase можно получить используя метод getPurchases.

val purchasesUseCase: PurchasesUseCase = billingClient.purchases
purchasesUseCase.getPurchases().addOnSuccessListener { purchases ->
purchases.forEach { purchase ->
yourApi.validate(purchase.subscriptionToken)
}

Подтверждение покупки

Продукты, требующие подтверждения

RuStore содержит продукты следующих типов.

  • SUBSCRIPTION — подписка (можно купить на период времени, например: подписка в стриминговом сервисе).
  • NON_CONSUMABLE — непотребляемый (можно купить один раз, например: отключение рекламы в приложении).
  • CONSUMABLE — потребляемый (можно купить много раз, например: кристаллы в приложении).
Подтверждения требуют только продукты типа CONSUMABLE, если они находятся в состоянии PurchaseState.PAID.

Вызов метода подтверждения

Для подтверждения покупки используйте метод confirmPurchase. Запрос на подтверждение покупки должен сопровождаться выдачей товара. После вызова подтверждения покупка перейдёт в статус CONSUMED.

val purchasesUseCase: PurchasesUseCase = billingClient.purchases
purchasesUseCase.confirmPurchase(purchaseId = "purchaseId", developerPayload = null)
.addOnSuccessListener {
// Process success
}.addOnFailureListener { throwable: Throwable ->
// Process error
}
  • purchaseId — идентификатор покупки.
  • developerPayload — указанная разработчиком строка, содержащая дополнительную информацию о заказе.

Отмена покупки

Для отмены покупки используйте метод deletePurchase.

val purchasesUseCase: PurchasesUseCase = billingClient.purchases
purchasesUseCase.deletePurchase(purchaseId = "purchaseId")
.addOnSuccessListener {
// Process success
}.addOnFailureListener { throwable: Throwable ->
// Process error
}
  • purchaseId — идентификатор покупки.
к сведению

Используйте этот метод, если у вас есть логика, завязанная на удалении покупки. Покупка отменяется автоматически через таймаут в 20 минут, либо при повторной покупке от того же клиента.

Обработка ошибок

При вызове метода RuStoreBillingClient.purchases.purchaseProduct(), ошибки обрабатываются автоматически.

Для показа диалога с ошибкой пользователю используйте метод resolveForBilling (см. ниже).

public fun RuStoreException.resolveForBilling(context: Context)

Обработка незавершённых платежей

Обработка незавершённых платежей производится разработчиком.

Чтобы подтвердить покупку типа CONSUMABLE и в статусе PAID вызовите метод подтверждения покупки (см. Получение сведений о покупке).

В случае с отменой покупки при использовании методов обработки платежей учитывайте свой внутренний процесс. У некоторых разработчиков он предусматривает проверки перед потреблением или отменой покупки. В этом случае запросите отдельно статус такой покупки.

подсказка

Например, если пользователь оплатил товар, который вы по каким-то причинам не можете ему поставить, вызовите метод отмены покупки в статусе PAID, чтобы отменить покупку.

В случаях, когда метод получения списка покупок возвращает покупку со статусом INVOICE_CREATED вы можете использовать метод отмены покупки. Например, если не хотите видеть покупку с такими статусами в списке покупок. Делать это самим не обязательно, поскольку RuStore обрабатывает отмену таких покупок на своей стороне.

к сведению

Иногда после оплаты через приложение банка (СБП, SberPay, Tinkoff Pay и др.) и при последующем возврате обратно в приложение статус покупки остаётся INVOICE_CREATED, при этом статус платежа — неуспешная покупка. Это связано с временем обработки покупки на стороне банка. Поэтому разработчику необходимо правильно связать логику получения списка покупок с жизненным циклом экрана.

Альтернативное решение — отмена покупки в статусе INVOICE_CREATED только через взаимодействие пользователя с приложением. Например, вы можете вынести эту логику в отдельную кнопку.

Ведение журнала событий

Если необходимо логировать события библиотеки платежей, добавьте в вызов RuStoreBillingClientFactory.create() параметры externalPaymentLoggerFactory и debugLogs. Они не обязательны для инициализации.

val billingClient: RuStoreBillingClient = RuStoreBillingClientFactory.create(
context = app,
consoleApplicationId = "111111",
deeplinkScheme = "yourappscheme",
externalPaymentLoggerFactory = { tag -> PaymentLogger(tag) },
debugLogs = true
)

class PaymentLogger(private val tag: String) : ExternalPaymentLogger {
override fun d(e: Throwable?, message: () -> String) {
Log.d(tag, message.invoke(), e)
}

override fun e(e: Throwable?, message: () -> String) {
Log.e(tag, message.invoke(), e)
}

override fun i(e: Throwable?, message: () -> String) {
Log.i(tag, message.invoke(), e)
}

override fun v(e: Throwable?, message: () -> String) {
Log.v(tag, message.invoke(), e)
}

override fun w(e: Throwable?, message: () -> String) {
Log.w(tag, message.invoke(), e)
}
}

Ниже представлены параметры для включения логирования.

  • externalPaymentLoggerFactory — интерфейс, позволяющий создать логгер, который пробрасывает логи библиотеки в приложение-хост;
  • debugLogs — включить логи (логи будут автоматически отключены для Release-сборок).

Здесь PaymentLogger — это пример реализации логирования событий платежей.

Смена темы интерфейса

SDK поддерживает динамическую смены темы через интерфейс провайдера BillingClientThemeProvider.

val billingClient: RuStoreBillingClient = RuStoreBillingClientFactory.create(
context = app,
consoleApplicationId = "111111",
deeplinkScheme = "yourappscheme",
themeProvider? = BillingClientThemeProviderImpl(),
)

class BillingClientThemeProviderImpl: BillingClientThemeProvider {

override fun provide(): BillingClientTheme {
// Тут должна размещаться логика по проверке установленной темы
val darkTheme = ....
if(darkTheme){
BillingClientTheme.Dark
} else {
BillingClientTheme.Light
}
}
}}
}

Обработка ошибок

Коды ошибок

Ниже представлено описание возможных ошибок в поле errorCode.

HTTP-кодКод ошибкиОписание
40040001Параметры запроса неверны — не заполнены обязательные параметры/неверный формат параметров.
40040003Приложение не найдено.
40040004Статус приложения inactive.
40040005Продукт не найден.
40040006Статус продукта inactive.
40040007Недопустимый тип продукта. Поддерживаемые типы: consumable, non-consumable, subscription.
40040008Покупка с таким order_id уже существует.
40040009У текущего клиента найдена покупка этого продукта со статусом invoice_created. Необходимо предложить клиенту оплатить/отменить покупку.
40040010Для типа продукта consumable. У текущего клиента найдена покупка этого продукта со статусом paid. Сначала требуется подтвердить потребление покупки на устройстве, а затем можно отправлять следующий запрос на покупку этого продукта.
40040011Для типа продукта non-consumable. У текущего клиента найдена покупка этого продукта со статусом pre_confirmed/confirmed. Такой продукт уже приобретён. Более одного раза продукт не продаётся.
40040012Для типа продукта subscription. У текущего клиента найдена покупка этого продукта со статусом pre_confirmed/confirmed. Такой продукт уже приобретён. Более одного раза продукт не продаётся.
40040013Для типа продукта subscription. При обращении в сервис подписок за списком продуктов GET/products (serviceId, user_id) данные не были получены.
40040014Обязательный атрибут(-ы) не пришел в запросе.
40040015Не удалось изменить статус при обновлении покупки (переход запрещён).
40040016При покупке подписки непотребляемого продукта указано значение quantity > 1.
40040017Продукт удалён, новые покупки не доступны.
40040018Нельзя потреблять продукт с типом тип продукта.
40140101Невалидный токен.
40140102Время жизни токена истекло.
40340301Доступ к запрашиваемому ресурсу запрещён (неавторизованно).
40340302Для текущего токена текущий вызов не авторизован (метод запрещён).
40340303Идентификатор приложения в запросе и токен не совпадают.
40340305Неверный тип токена.
40440401Не найдено.
40840801Истекло время ожидания уведомления, указанное в запросе.
50050***Внутренняя ошибка платежного сервиса.