
Tutorial
September 7, 2022
34 min read
September 7, 2022
14 min read
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:
Zanim zaczniemy, upewnij się, że:
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.
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):
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.
Gdy produkty są już utworzone, popracujmy nad architekturą akceptowania i przetwarzania zakupów. Ogólnie rzecz biorąc, proces wygląda następująco:
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.
Further reading
Tutorial
September 7, 2022
34 min read
Tutorial
September 7, 2022
19 min read
Tutorial
September 7, 2022
14 min read