Привіт. Я zm soft — зареєструвався як розробник минулого року (наприкінці 2023) і почав випускати застосунки. Також плануємо випустити застосунок для розробників, щоб спільно подолати закрите тестування через співпрацю між розробниками, тому ознайомтеся, якщо цікаво.
Чи реалізовували ви внутрішні покупки в застосунку? Для розробників застосунків найкраще — коли користувачі задоволені і платять за це; саме в цей момент зусилля розробки отримують нагороду. Цього разу хочу написати про те, як це реалізувати.
Що таке внутрішні покупки в застосунку
Як ви знаєте, внутрішні покупки поділяються на два типи:
- Витратні елементи
- Елементи з періодичною підпискою
Обидва використовують підхід до доступу до елементів, попередньо налаштованих у Play Store через бібліотеку Google Play Billing Library, і частина отриманого доходу виплачується Google як комісія. З точки зору реалізації обидва схожі, але елементи з підпискою мають власні застереження. Я поясню їх на основі свого особистого досвіду.
Налаштування Play Store
Реєстрація елементів
Спочатку зареєструйте елементи, оскільки без реєстрації їх не можна відображати. Ціна, назва тощо, а також те, чи будете ви насправді використовувати цей елемент, можна змінити пізніше, тому тимчасові дані підійдуть. Однак лише ID змінити не можна, тому якщо ви плануєте використовувати елементи, створені для тестування, у виробничому середовищі після їх модифікації, краще зареєструвати їх із багаторазово використовуваними ID, наприклад [product_1,2,3...] або [sub_1,2,3...]. Реєстрація може здійснюватися через [Monetize], де [In-app products] і [Subscriptions] відповідно є витратними елементами та елементами з підпискою. Кожен елемент можна зареєструвати за допомогою кнопки [Create xx].

Загалом, можна слідувати екрану введення, але збентежило мене налаштування цін для елементів з підпискою. Спочатку я не знав, як встановити ціни для всіх регіонів одразу. Думав, що доведеться вводити кожен регіон окремо, але виявилося, що це можна зробити в такому порядку: [Set prices] - [Country / region] - [Set price].

При натисканні кнопки з'являється наступний діалог, що дозволяє налаштувати все одразу.

Реєстрація тестового облікового запису
Далі зареєструйте тестовий обліковий запис. Якщо ви зробите тестову покупку без реєстрації, відбудеться реальна оплата, тому будьте обережні. Реєстрація тестового облікового запису здійснюється через реєстрацію списку розсилки. Реєстрацію можна зробити на першому екрані (екран розробника) після входу до Play Console через [Settings] - [License testing].

Коли процес покупки застосунку виконується з користувачем, зареєстрованим у списку розсилки, інформація про оплату відображається як «тестова картка», що дозволяє тестувати без реальної оплати.
Робота з реалізації
Реалізація на стороні застосунку вимагає такої роботи:
- Імпорт бібліотеки
- Підключення до магазину та отримання інформації про елементи
- Реалізація екрану відображення елементів
- Запит на покупку
- Обробка після завершення покупки
Нижче детально опишу кожен пункт.
Імпорт бібліотеки
Для імпорту бібліотеки необхідно:
- Внести зміни до build.gradle
- Внести зміни до android.manifest
Приклад змін у build.gradle:
dependencies {
implementation "com.android.billingclient:6.0.0"
}
Приклад змін у manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="com.android.vending.BILLING" />
<application
Щодо версії тощо, будь ласка, зверніться до офіційної документації.
Підключення до магазину та отримання інформації про елементи
Ініціалізувавши BillingClient та запустивши startConnection, стає можливим спілкування з магазином. Інформацію про кожен елемент можна отримати за допомогою queryProductDetailsAsync. Для деталей, будь ласка, зверніться до офіційної документації. У моєму випадку масове отримання чомусь не спрацювало, тому я спочатку отримав список елементів, а потім за допомогою queryProductDetailAsync отримав деталі кожного елемента окремо. Отриманий елемент (SKU) можна визначити як витратний або підписний за його типом — inapp або subs.
Реалізація екрану відображення елементів
Після отримання інформації про елементи відобразіть її у вигляді списку для можливості вибору покупки. При роботі з елементами підписки тут необхідно дотримуватися Subscription Policy, і в моєму випадку оновлення застосунку послідовно відхилялися з двох причин:
- Неповна локалізація ціни та умов
- Неповний опис умов пропозиції
Спочатку про неповну локалізацію: причиною було відображення періоду (приклад: $10/month). Проблема в тому, що слово «month» не перекладалося при роботі з налаштуваннями регіону мови, яка не підтримується в перекладі. При отриманні інформації за допомогою BillingClient сума ціни отримується як рядок formattedPrice, що включає інформацію про одиницю, тому немає потреби перекладати її в застосунку. Натомість інформація про період надходить у форматі ISO 8601, наприклад P1M, і це потрібно перекладати в застосунку. Я вирішив це, додавши процеси перетворення для кожної мови:
fun formatBillingPeriod(billingPeriod: String, languageCode: String): String {
return when(languageCode) {
"en" -> {
when (billingPeriod) {
"P1W" -> "weekly"
"P1M" -> "monthly"
"P3M" -> "every 3 months"
"P6M" -> "every 6 months"
"P1Y" -> "annually"
else -> "unknown"
}
}
"ja" -> {
when (billingPeriod) {
"P1W" -> "週間"
"P1M" -> "月額"
"P3M" -> "3ヶ月ごと"
"P6M" -> "6ヶ月ごと"
"P1Y" -> "年額"
else -> "不明"
}
}
"fr" -> {
when (billingPeriod) {
"P1W" -> "hebdomadaire"
"P1M" -> "mensuel"
"P3M" -> "tous les 3 mois"
"P6M" -> "tous les 6 mois"
"P1Y" -> "annuel"
else -> "inconnu"
}
}
"es" -> {
when (billingPeriod) {
"P1W" -> "semanal"
"P1M" -> "mensual"
"P3M" -> "cada 3 meses"
"P6M" -> "cada 6 meses"
"P1Y" -> "anual"
else -> "desconocido"
}
}
"de" -> {
when (billingPeriod) {
"P1W" -> "wöchentlich"
"P1M" -> "monatlich"
"P3M" -> "alle 3 Monate"
"P6M" -> "alle 6 Monate"
"P1Y" -> "jährlich"
else -> "unbekannt"
}
}
else -> "unknown"
}
}
Далі про неповний опис умов пропозиції:

Я зрозумів проблему, але не знав точно, які конкретні слова потрібно написати як рішення. Нижче наведено витяг з оригінального тексту електронного листа, в якому я отримав сповіщення про відхилення:
Issue found: Violation of Subscriptions policy
Your app does not comply with the Subscriptions policy.
- Your offer does not clearly and accurately describe the terms of your trial offer or introductory pricing, including when a free trial will convert to a paid subscription, how much the paid subscription will cost, and that a user can cancel if they do not want to convert to a paid subscription.
Як рішення, я запозичив текст з інших застосунків. Оскільки він здається переважно стандартним текстом, я вирішив, що краще слідувати перевіреним прецедентам, ніж думати з нуля.
Запит на покупку
Після того, як користувач може вибрати елемент з екрану, нарешті настає обробка покупки елемента. Виконавши потік покупки для отриманого елемента, можна викликати реальний екран покупки.
Тестування
Тестування можна проводити без реальних витрат, увійшовши за допомогою тестового облікового запису, підготовленого на початку. Два пункти уваги тут такі:
- Виконання потоку покупки здійснюється з застосунком, зареєстрованим у магазині
- Для елементів з підпискою встановлюється спеціальний період
Стосовно виконання потоку покупки: якщо застосунок є debug-збіркою, на екрані покупки з'явиться помилка і потік не можна буде запустити.
Стосовно елементів з підпискою: при переході до реального екрану покупки відображається оплата тестовою карткою. При цьому на екрані покупки відображається період оновлення, але цей відображений період відрізняється від реально налаштованого для елемента. У моєму випадку відображалося 5 хвилин. Схоже, це специфікація відображення надзвичайно короткого фіксованого періоду для перевірки поведінки оновлення підписки. (Я думав, що десь помилився в налаштуваннях і деякий час переглядав їх.)
Висновок
Спочатку здається складним, але після розуміння загальної картини реалізація внутрішніх покупок сама по собі може бути несподівано легкою за допомогою загальнодоступної інформації, як-от офіційна документація. Однак для підписок є пастки, які неможливо виявити, не спробувавши, тому сподіваюся, що моменти, де я спіткнувся, стануть орієнтиром для розробників, які будуть це реалізовувати в майбутньому.