Subscriptions 101: learn how to get +23% app revenue Read more

Zakupy w aplikacji na Androida, Część 1: konfiguracja i dodawanie do projektu

Vlad Guriev

September 7, 2022

14 min read

Table of Contents

62fdf1dbf601140886432256 jp android tutorial 1 configuration 1

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:

  1. Posiadasz konto deweloperskie dla Google Play.
  2. 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ę.

60fef7a69f036b3bfc996258 jsaoezesquwst5gknnib6kc3xqckd8yldedvgpfkaagrqbg9xksbckpwo

Następnie w menu po lewej stronie znajdź sekcję Products, wybierz Subscriptions i kliknij Create a Subscription.

60fef7a7b3b5c7c37e286978 qy go34alibtdmnfdkaii3wg1fcjv5bexstyoefeazq 2oagsnx37gndqykcx6rcwcyu7ahpfzug gusku2bg0sxtq nkmglkw5uuvlabnnlnmp7j2ljfc9 nkzhyhbhegp1nl8r

Następnie zobaczysz konfigurator subskrypcji. Oto kilka ważnych elementów.

60fef7a7d069f50dac0202a5 edgfvm0dhtqew hin9r1uqwvyzuzna1nw4auk hsl8kvgoc2uev8ulj5yhinnvdwv6xau1c1kzy2vd0fmpc8ezyurr3vtl 1qbj19ewbfwqspewed3d echiz1gjqfrfpeiwm0yb
  1. 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.
  2. Nazwę subskrypcji, którą użytkownik zobaczy w sklepie.  
  3. Opis subskrypcji. Użytkownik ją też zobaczy.
60fef7a755430242d074b04c 5mix72nt8rn7nbjrkxd7l7pfit6yuebw9f8x0bke47mx1pbvvagpf4uar w dlkpkkfl9q7udzodqctd451avnwnvqccvtmz0wwj90f8md8mbmrl464wo

Przewiń w dół i wybierz okres subskrypcji. W naszym przypadku będzie to tydzień. Określ cenę.

60fef7a8d11d44258c894e7d 658mke hovj itrvd6mamegcdp6wvefdt0bdamvvssnwg0cca

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.

60fef7a7ebfe90328d0a6a28 3k q7t5g7luv4axvw25thwlj3aibgsnnkahdtfeleaba31hqwgvo1yin2qayyh1r97tp4p nyvavypbvrt2

Przewiń w dół i wybierz (w razie potrzeby): 

  1. Bezpłatny okres próbny.
  2. Cena wstępna, będąca ofertą dla pierwszego okresu płatności.  
  3. Okres karencji. Jeśli użytkownik ma problemy z płatnością, możesz zapewnić mu dostęp premium na pewną liczbę dni.
  4. 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:   

  1. Dodawanie Billing Library.
  2. Opracowywanie klasy do interakcji z produktami z Google Play.
  3. Implementacja wszystkich metod przetwarzania zakupów.  
  4. Dodawanie walidacji zakupu na serwerze.  
  5. 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.