Zakupy w aplikacji na Androida, Część 1: konfiguracja i dodawanie do projektu
Updated: 20 marca, 2023
Zakupy w aplikacji (in-app purchases), zwłaszcza subskrypcje (subscriptions), są najpopularniejszymi metodami zarabiania na aplikacji. Z jednej strony subskrypcja pozwala deweloperowi inwestować w rozwój treści i produktu, z drugiej strony pomaga użytkownikom uzyskać aplikację o wyższym poziomie jakości. Zakupy w aplikacji są obłożone 30% prowizji, ale jeśli użytkownik pozostaje subskrybentem przez dłużej, niż rok lub aplikacja zarabia mniej niż 1 mln USD rocznie, prowizja wynosi 15%.
W tym artykule wyjaśnimy następujące tematy:
- Tworzenie produktu w Google Play Console;
- Konfiguracja subskrypcji: jak określić czas trwania, cenę, wersje próbne;
- Dodawanie listy produktów w aplikacji.
Tworzenie subskrypcji
Zanim zaczniemy, upewnij się, że:
- Posiadasz konto deweloperskie dla Google Play.
- Podpisałeś wszystkie umowy i jesteś gotów do rozpoczęcia pracy.
A teraz, przejdźmy do działania i stwórzmy nasz pierwszy produkt.
Przełącz się do swojego konta dewelopera i wybierz aplikację.
Następnie w menu po lewej stronie znajdź sekcję Products, wybierz Subscriptions i kliknij Create a Subscription.
Następnie zobaczysz konfigurator subskrypcji. Oto kilka ważnych elementów.
- Utwórz ID, które zostanie wykorzystane w aplikacji. Dobrym pomysłem jest dodanie okresu subskrypcji lub innych przydatnych informacji do ID, dzięki czemu będziesz mógł tworzyć produkty w jednym stylu i ułatwić sobie analizowanie statystyk sprzedaży.
- Nazwę subskrypcji, którą użytkownik zobaczy w sklepie.
- Opis subskrypcji. Użytkownik ją też zobaczy.
Przewiń w dół i wybierz okres subskrypcji. W naszym przypadku będzie to tydzień. Określ cenę.
Zazwyczaj cenę określa się w głównej walucie konta, a system automatycznie przelicza cenę. Możesz także ręcznie edytować cenę dla określonego kraju.
Zwróć uwagę, że Google pokazuje podatek dla każdego kraju. To świetnie, App Store Connect tego nie robi.
Przewiń w dół i wybierz (w razie potrzeby):
- Bezpłatny okres próbny.
- Cena wstępna, będąca ofertą dla pierwszego okresu płatności.
- Okres karencji. Jeśli użytkownik ma problemy z płatnością, możesz zapewnić mu dostęp premium na pewną liczbę dni.
- Możliwość ponownej subskrypcji po anulowaniu z Play Store, a nie z aplikacji.
Porównanie procesu zakupu w Play Console i App Store Connect
Pomimo tego, że subskrypcje można bardziej efektywnie monetyzować na iOS, tablica administracyjna Play Console jest wygodniejsza, lepiej zorganizowana i przetłumaczona, a ponadto działa szybciej.
Proces tworzenia produktu jest tak prosty, jak to tylko możliwe. Tutaj opowiedzieliśmy, jak tworzyć produkty na iOS.
Uzyskiwanie listy produktów w aplikacji
Gdy produkty są już utworzone, popracujmy nad architekturą akceptowania i przetwarzania zakupów. Ogólnie rzecz biorąc, proces wygląda następująco:
- Dodawanie Billing Library.
- Opracowywanie klasy do interakcji z produktami z Google Play.
- Implementacja wszystkich metod przetwarzania zakupów.
- Dodawanie walidacji zakupu na serwerze.
- Zbieranie dane analityczne.
W tej części przyjrzyjmy się bliżej dwóm pierwszym punktom.
Dodawanie Billing Library do projektu:
implementation "com.android.billingclient:billing:4.0.0"
W chwili pisania tego tekstu najnowszą wersją jest 4.0.0. Możesz go zastąpić dowolną inną wersją w dowolnym momencie.
Stwórzmy klasę wrapper, która obejmie logikę interakcji z Google Play i zainicjuje BillingClient z Billing Library w niej. Nazwijmy to klasą BillingClientWrapper.
Ta klasa zaimplementuje interfejs PurchasesUpdatedListener. Teraz nadpiszemy dla niej metodę – onPurchasesUpdated (billingResult: BillingResult, purchaseList: MutableList<zakup>?) – jest to potrzebne od razu po dokonaniu zakupu, ale proces wdrożenia opiszemy w następnym artykule.
import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
private val billingClient = BillingClient
.newBuilder(context)
.enablePendingPurchases()
.setListener(this)
.build()
override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
// here come callbacks about new purchases
}
}
Google poleca, aby unikać posiadania więcej, niż jednego aktywnego połączenia między BillingClient i Google Play, dla zapobiegania kilkukrotnemu wykonywaniu się powiadomienia o zakupie. Z tego powodu powinieneś mieć jeden unikalny BillingClient w klasie singleton. Klasa w przykładzie nie jest singletonem, ale możemy użyć wstrzykiwanie zależności (na przykład przy pomocy Dagger lub Koin) w ten sposób, pozwalając na istnienie tylko jednej instancji w jednym momencie.
Aby wykonać żądanie w Billing Library, BillingClient musi mieć aktywne połączenie z Google Play w momencie składania wniosku, ale połączenie może zostać utracone w pewnym momencie. Dla wygody napiszmy wrapper pozwalający nam na składanie żądań tylko wtedy, gdy połączenie jest aktywne.
Aby uzyskać produkty, potrzebujemy ich identyfikatorów, które ustawiliśmy na rynku. Ale tego nie wystarczy, aby stworzyć żądanie, gdyż potrzebujemy również typu produktu (subskrypcja lub jednorazowy zakup), dlatego możemy uzyskać ogólną listę produktów, „łącząc” wyniki dwóch zapytań.
Żądanie produktów jest asynchroniczne, więc potrzebujemy wywołania zwrotnego, które dostarczy nam listę produktów lub zwróci model błędu. Gdy wystąpi błąd, Billing Library zwróci jeden z BillingResponseCodes, a także debugMessage. Stwórzmy interfejs wywołania zwrotnego i model błędu:
interface OnQueryProductsListener {
fun onSuccess(products: List < SkuDetails > )
fun onFailure(error: Error)
}
class Error(val responseCode: Int, val debugMessage: String)
Oto kod prywatnej metody pobierania danych o określonym typie produktów i publicznej metody, która „połączy” wyniki dwóch zapytań i dostarczy użytkownikowi ostateczną listę produktów lub wyświetli komunikat o błędzie.
fun queryProducts(listener: OnQueryProductsListener) {
val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
queryProductsForType(
skusList,
BillingClient.SkuType.SUBS
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val products = skuDetailsList ?: mutableListOf()
queryProductsForType(
skusList,
BillingClient.SkuType.INAPP
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
products.addAll(skuDetailsList ?: listOf())
listener.onSuccess(products)
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
}
private fun queryProductsForType(
skusList: List<String>,
@BillingClient.SkuType type: String,
listener: SkuDetailsResponseListener
) {
onConnected {
billingClient.querySkuDetailsAsync(
SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
listener
)
}
}
W ten sposób otrzymaliśmy cenne informacje o produktach (SkuDetails), gdzie możemy zobaczyć lokalizowane nazwy, ceny, rodzaj produktu, a także okres rozliczeniowy i informacje o cenie początkowej i okresie próbnym (jeśli jest dostępny dla tego użytkownika) dla subskrypcji. Oto jak wygląda ostateczna klasa:
import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
interface OnQueryProductsListener {
fun onSuccess(products: List<SkuDetails>)
fun onFailure(error: Error)
}
class Error(val responseCode: Int, val debugMessage: String)
private val billingClient = BillingClient
.newBuilder(context)
.enablePendingPurchases()
.setListener(this)
.build()
fun queryProducts(listener: OnQueryProductsListener) {
val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
queryProductsForType(
skusList,
BillingClient.SkuType.SUBS
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val products = skuDetailsList ?: mutableListOf()
queryProductsForType(
skusList,
BillingClient.SkuType.INAPP
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
products.addAll(skuDetailsList ?: listOf())
listener.onSuccess(products)
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
}
private fun queryProductsForType(
skusList: List<String>,
@BillingClient.SkuType type: String,
listener: SkuDetailsResponseListener
) {
onConnected {
billingClient.querySkuDetailsAsync(
SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
listener
)
}
}
private fun onConnected(block: () -> Unit) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
block()
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
// here come callbacks about new purchases
}
}
To wszystko na dziś. W kolejnych artykułach opowiemy o implementacji zakupu, testowaniu i obsłudze błędów.