Sprzedawanie subskrypcji w aplikacji przy użyciu Biblioteki płatności w Play 5

1. Wprowadzenie

System rozliczeniowy Google Play to usługa umożliwiająca sprzedaż produktów i treści cyfrowych w aplikacji na Androida. To najbardziej bezpośredni sposób sprzedaży produktów w aplikacji, który pozwala zarabiać na aplikacji. Dzięki temu ćwiczeniu w Codelabs dowiesz się, jak używać Biblioteki płatności w Google Play do sprzedaży subskrypcji w swoim projekcie w sposób uwzględniający najważniejsze szczegóły podczas integrowania zakupów z pozostałymi elementami aplikacji.

Szkolenie zawiera też omówienie zagadnień związanych z subskrypcjami, takich jak abonamenty podstawowe, oferty, tagi i abonamenty przedpłacone. Więcej informacji o subskrypcjach w Płatnościach w Google Play znajdziesz w Centrum pomocy.

Co utworzysz

Z tego ćwiczenia w Codelabs dowiesz się, jak dodać najnowszą bibliotekę płatności (w wersji 5.0.0) do prostej aplikacji profilowej opartej na subskrypcji. Ta aplikacja została już stworzona dla Ciebie, musisz więc dodać tylko część rozliczeniową. Jak widać na ilustracji 1, w tej aplikacji użytkownik rejestruje się w dowolnym z abonamentów podstawowych lub ofert oferowanych w ramach 2 subskrypcji odnawialnych (podstawowej i premium) lub w przypadku nieodnawialnej karty przedpłaconej. To wszystko. Abonamenty podstawowe to odpowiednio subskrypcje miesięczne i roczne. Użytkownik może przejść na wyższą lub niższą wersję usługi przedpłaconej albo przekształcić ją w subskrypcję odnawialną.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

Aby dodać Bibliotekę płatności w Google Play do swojej aplikacji, musisz utworzyć:

  • BillingClientWrapper – kod biblioteki BillingClient. Ma ona obejmować interakcje z klientem rozliczeniowym Biblioteki płatności w Play, ale nie jest to wymagane w Twojej integracji.
  • SubscriptionDataRepository – repozytorium rozliczeń aplikacji, które zawiera listę asortymentu produktów objętych subskrypcją (tj. produktów na sprzedaż) oraz listę zmiennych ShareFlow, które pomagają zbierać informacje o stanie zakupów i szczegółach produktów
  • MainViewModel – model ViewModel, za pomocą którego reszta aplikacji komunikuje się z repozytorium płatności. Pomaga uruchomić w interfejsie proces płatności za pomocą różnych metod zakupu.

Gdy skończysz, architektura aplikacji powinna wyglądać podobnie do tej na ilustracji:

c83bc759f32b0a63.png

Czego się nauczysz

  • Jak zintegrować Bibliotekę płatności w Google Play
  • Jak tworzyć produkty objęte subskrypcją, abonamenty podstawowe, oferty i tagi w Konsoli Play
  • Jak pobrać dostępne abonamenty podstawowe i oferty z aplikacji
  • Jak uruchomić proces płatności za pomocą odpowiednich parametrów
  • Jak oferować produkty objęte subskrypcją przedpłaconą

To ćwiczenia w Codelabs dotyczą płatności w Google Play. Nieistotne koncepcje i bloki kodu zostały zamaskowane. Można je po prostu skopiować i wkleić.

Czego potrzebujesz

  • Najnowsza wersja Android Studio (>= Arctic Fox | 2020.3.1)
  • urządzenie z Androidem w wersji 8.0 lub nowszej,
  • Przykładowy kod udostępniony na GitHubie (instrukcje znajdziesz w dalszych sekcjach)
  • Umiarkowana znajomość aplikacji na Androida w Android Studio
  • Wiedza o tym, jak opublikować aplikację w Sklepie Google Play
  • Umiarkowane doświadczenie w pisaniu kodu Kotlin
  • Biblioteka Płatności w Google Play w wersji 5.0.0

2. Przygotowanie

Pobierz kod z GitHub

Wszystko, czego potrzebujesz do tego projektu, umieściliśmy w repozytorium Git. Aby rozpocząć, pobierz kod i otwórz go w ulubionym środowisku programistycznym. Na potrzeby tego ćwiczenia w Codelabs zalecamy korzystanie z Android Studio.

Kod, który należy rozpocząć, jest przechowywany w repozytorium GitHub. Możesz sklonować repozytorium za pomocą tego polecenia:

git clone https://github.com/android/play-billing-samples.git
cd PlayBillingCodelab

3. Podstawy

Od czego zacząć?

Na początek zajmiemy się podstawową aplikacją do tworzenia profilu użytkownika, która powstała na potrzeby tego ćwiczenia z programowania. Kod został uproszczony, by pokazać koncepcje, które chcemy zilustrować, i nie jest przeznaczony do użytku w środowisku produkcyjnym. Jeśli chcesz ponownie wykorzystać dowolną część tego kodu w aplikacji produkcyjnej, postępuj zgodnie ze sprawdzonymi metodami i w pełni przetestuj swój kod.

Zaimportuj projekt do Android Studio

PlayBillingCodelab to aplikacja podstawowa, która nie zawiera implementacji Płatności w Google Play. Uruchom Android Studio i zaimportuj ćwiczenia z programowania do płatności, wybierając Open > billing/PlayBillingCodelab

Projekt obejmuje 2 moduły:

  • Parametr start zawiera aplikację typu szkielet, ale nie ma w nim wymaganych zależności i metod, które musisz wdrożyć.
  • Użytkownik finished ma ukończony projekt i może służyć jako wskazówka, jeśli utkniesz w jakimś miejscu.

Aplikacja składa się z 8 plików zajęć: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory i MainActivity.

  • BillingClientWrapper to kod, który wyodrębnia metody płatności [BillingClient] w Płatnościach w Google Play i przesyła odpowiedzi do repozytorium danych w celu ich przetworzenia.
  • Parametr SubscriptionDataRepository służy do wyodrębniania źródła danych Płatności w Google Play (tj. z biblioteki klienta rozliczeniowego) i przekształcanie danych StateFlow pochodzących z usługi BillingClientWrapper na przepływy.
  • ButtonModel to klasa danych używana do tworzenia przycisków w interfejsie.
  • Obiekty Composable wyodrębniają wszystkie metody kompozycyjne interfejsu użytkownika do jednej klasy.
  • MainState to klasa danych zarządzania stanem.
  • Model MainViewModel służy do przechowywania danych dotyczących płatności i stanów używanych w interfejsie użytkownika. Łączy wszystkie przepływy w SubscriptionDataRepository w jednym obiekcie stanu.
  • MainActivity to główna klasa aktywności, która wczytuje elementy kompozycyjne interfejsu użytkownika.
  • Stałe to obiekt zawierający stałe używane przez wiele klas.

Gradle

Aby dodać do aplikacji Płatności w Google Play, musisz dodać zależność Gradle. Otwórz plik build.gradle modułu aplikacji i dodaj ten kod:

dependencies {
    val billing_version = "5.0.0"

    implementation("com.android.billingclient:billing:$billing_version")
}

Konsola Google Play

Na potrzeby tego ćwiczenia w Codelabs musisz utworzyć w sekcji Subskrypcje w Konsoli Google Play te 2 oferty usług subskrypcji:

  • 1 subskrypcja podstawowa o identyfikatorze produktu up_basic_sub

Usługa powinna mieć 3 abonamenty podstawowe (2 automatycznie odnawiane i 1 przedpłacony) z powiązanymi tagami : 1 miesięczną subskrypcję podstawową z tagiem monthlybasic, 1 roczną subskrypcję podstawową z tagiem yearlybasic i 1 subskrypcję przedpłaconą z tagiem prepaidbasic

Do abonamentów podstawowych możesz dodawać oferty. Oferty odziedziczą tagi z powiązanych abonamentów podstawowych.

  • 1 subskrypcja premium o identyfikatorze produktu up_premium_sub

Usługa powinna mieć 3 abonamenty podstawowe(2 automatycznie odnawiane i 1 przedpłacony) z powiązanymi tagami: 1 miesięczną subskrypcję podstawową z tagiem monthlypremium, 1 roczną subskrypcję podstawową z tagiem yearlypremium i 1 subskrypcję przedpłaconą z tagiem prepaidpremium

a9f6fd6e70e69fed.png

Do abonamentów podstawowych możesz dodawać oferty. Oferty odziedziczą tagi z powiązanych abonamentów podstawowych.

Więcej informacji o tworzeniu usług objętych subskrypcją, abonamentów podstawowych, ofert i tagów znajdziesz w Centrum pomocy Google Play.

4. Konfiguracja klienta rozliczeniowego

W tej sekcji będziesz pracować w zajęciach BillingClientWrapper.

Na koniec będziesz mieć wszystkie informacje potrzebne do utworzenia instancji klienta rozliczeniowego oraz wszystkie powiązane z nim metody.

  1. Inicjowanie klienta BillingClient

Po dodaniu zależności do Biblioteki płatności w Google Play musimy zainicjować instancję BillingClient.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Nawiązywanie połączenia z Google Play

Po utworzeniu klienta BillingClient musimy połączyć się z Google Play.

Aby połączyć się z Google Play, nazywamy się startConnection(). Proces łączenia jest asynchroniczny i gdy klient będzie gotowy na wysyłanie kolejnych żądań, musi wdrożyć funkcję BillingClientStateListener, aby otrzymać wywołanie zwrotne.

BillingClientWrapper.kt

fun startBillingConnection(billingConnectionState: MutableLiveData<Boolean>) {

   billingClient.startConnection(object : BillingClientStateListener {
       override fun onBillingSetupFinished(billingResult: BillingResult) {
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               Log.d(TAG, "Billing response OK")
               // The BillingClient is ready. You can query purchases and product details here
               queryPurchases()
               queryProductDetails()
               billingConnectionState.postValue(true)
           } else {
               Log.e(TAG, billingResult.debugMessage)
           }
       }

       override fun onBillingServiceDisconnected() {
           Log.i(TAG, "Billing connection disconnected")
           startBillingConnection(billingConnectionState)
       }
   })
}
  1. Wysyłanie zapytań do Płatności w Google Play w przypadku dotychczasowych zakupów

Po nawiązaniu połączenia z Google Play możemy wysyłać zapytania dotyczące zakupów dokonanych przez użytkownika wcześniej, kontaktując się z firmą queryPurchasesAsync().

BillingClientWrapper.kt

fun queryPurchases() {
   if (!billingClient.isReady) {
       Log.e(TAG, "queryPurchases: BillingClient is not ready")
   }
   // Query for existing subscription products that have been purchased.
   billingClient.queryPurchasesAsync(
       QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
   ) { billingResult, purchaseList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           if (!purchaseList.isNullOrEmpty()) {
               _purchases.value = purchaseList
           } else {
               _purchases.value = emptyList()
           }

       } else {
           Log.e(TAG, billingResult.debugMessage)
       }
   }
}
  1. Wyświetlanie produktów dostępnych do zakupu

Możemy teraz wysyłać zapytania o dostępne produkty i wyświetlać je użytkownikom. Aby wysyłać do Google Play zapytania o szczegóły produktów objętych subskrypcją, zadzwonimy pod queryProductDetailsAsync(). Zapytanie o szczegóły produktu jest ważnym krokiem przed wyświetleniem produktów użytkownikom, ponieważ zwraca zlokalizowane informacje o produkcie.

BillingClientWrapper.kt

fun queryProductDetails() {
   val params = QueryProductDetailsParams.newBuilder()
   val productList = mutableListOf<QueryProductDetailsParams.Product>()
   for (product in LIST_OF_PRODUCTS) {

       productList.add(
           QueryProductDetailsParams.Product.newBuilder()
               .setProductId(product)
               .setProductType(BillingClient.ProductType.SUBS)
               .build()
       )

       params.setProductList(productList).let { productDetailsParams ->
           Log.i(TAG, "queryProductDetailsAsync")
           billingClient.queryProductDetailsAsync(productDetailsParams.build(), this)
       }
   }
}
  1. Ustawianie detektora zapytania ProductDetails

Uwaga: ta metoda generuje wynik zapytania w mapie na _productWithProductDetails.

Pamiętaj też, że zapytanie powinno zwrócić wartość ProductDetails. Jeśli tak się nie stanie, najprawdopodobniej usługi skonfigurowane w Konsoli Play nie zostały aktywowane lub nie została opublikowana kompilacja uzależniona od płatności klienta na żadnej ścieżce wersji.

BillingClientWrapper.kt

override fun onProductDetailsResponse(
   billingResult: BillingResult,
   productDetailsList: MutableList<ProductDetails>
) {
   val responseCode = billingResult.responseCode
   val debugMessage = billingResult.debugMessage
   when (responseCode) {
       BillingClient.BillingResponseCode.OK -> {
           var newMap = emptyMap<String, ProductDetails>()
           if (productDetailsList.isNullOrEmpty()) {
               Log.e(
                   TAG,
                   "onProductDetailsResponse: " +
                           "Found null or empty ProductDetails. " +
                           "Check to see if the Products you requested are correctly " +
                           "published in the Google Play Console."
               )
           } else {
               newMap = productDetailsList.associateBy {
                   it.productId
               }
           }
           _productWithProductDetails.value = newMap
       }
       else -> {
           Log.i(TAG, "onProductDetailsResponse: $responseCode $debugMessage")
       }
   }
}
  1. Rozpoczynanie procesu zakupu

launchBillingFlow to metoda wywoływana, gdy użytkownik kliknie, by kupić produkt. Prosisz Google Play o rozpoczęcie procesu zakupu za pomocą ProductDetails produktu.

BillingClientWrapper.kt

fun launchBillingFlow(activity: Activity, params: BillingFlowParams) {
   if (!billingClient.isReady) {
       Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
   }
   billingClient.launchBillingFlow(activity, params)

}
  1. Ustaw detektor wyniku operacji zakupu

Gdy użytkownik wyjdzie z ekranu zakupu w Google Play (klikając przycisk „Kup”, aby dokończyć zakup, lub klikając przycisk Wstecz, aby anulować zakup), wywołanie zwrotne onPurchaseUpdated() wyśle z powrotem do Twojej aplikacji wynik procesu zakupu. Na podstawie tych danych (BillingResult.responseCode) możesz określić, czy użytkownik kupił produkt. Jeśli responseCode == OK, oznacza to, że zakup został zrealizowany.

onPurchaseUpdated() przekazuje listę Purchase obiektów, która obejmuje wszystkie zakupy dokonane przez użytkownika w aplikacji. Wśród wielu innych pól każdy obiekt purchase zawiera atrybuty identyfikator produktu, purchaseToken i isAcknowledged. Korzystając z tych pól, w przypadku każdego obiektu Purchase możesz określić, czy jest to nowy zakup, który trzeba przetworzyć, czy istniejący zakup, który nie wymaga dalszego przetwarzania.

W przypadku zakupu subskrypcji przetwarzanie jest jak potwierdzenie nowego zakupu.

BillingClientWrapper.kt

override fun onPurchasesUpdated(
   billingResult: BillingResult,
   purchases: List<Purchase>?
) {
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
       && !purchases.isNullOrEmpty()
   ) {
       // Post new purchase List to _purchases
       _purchases.value = purchases

       // Then, handle the purchases
       for (purchase in purchases) {
           acknowledgePurchases(purchase)
       }
   } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
       // Handle an error caused by a user cancelling the purchase flow.
       Log.e(TAG, "User has cancelled")
   } else {
       // Handle any other error codes.
   }
}
  1. Przetwarzanie zakupów (weryfikowanie i potwierdzanie zakupów)

Gdy użytkownik dokona zakupu, aplikacja musi go potwierdzić, potwierdzając jego realizację.

Dodatkowo po pomyślnym przetworzeniu potwierdzenia atrybut _isNewPurchaseAcknowledged ma wartość „true” (prawda).

BillingClientWrapper.kt

private fun acknowledgePurchases(purchase: Purchase?) {
   purchase?.let {
       if (!it.isAcknowledged) {
           val params = AcknowledgePurchaseParams.newBuilder()
               .setPurchaseToken(it.purchaseToken)
               .build()

           billingClient.acknowledgePurchase(
               params
           ) { billingResult ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
                   it.purchaseState == Purchase.PurchaseState.PURCHASED
               ) {
                   _isNewPurchaseAcknowledged.value = true
               }
           }
       }
   }
}
  1. Zakończ połączenie z rozliczeniami

Na koniec, gdy działanie zostanie zniszczone, chcesz przerwać połączenie z Google Play, więc polecenie endConnection() jest do tego wywoływane.

BillingClientWrapper.kt

fun terminateBillingConnection() {
   Log.i(TAG, "Terminating connection")
   billingClient.endConnection()
}

5. Repozytorium subskrypcji danych

W BillingClientWrapper odpowiedzi użytkowników QueryPurchasesAsync i QueryProductDetails są publikowane odpowiednio do MutableStateFlow _purchases i _productWithProductDetails, które są widoczne poza zajęciami i wraz z zakupami oraz productWithProductDetails.

W SubscriptionDataRepository zakupy są przetwarzane na 3 procesy w zależności od zwracanego produktu: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium i hasPremiumPrepaid.

Dodatkowo productWithProductDetails jest przetwarzany w odpowiednich przepływach basicProductDetails i premiumProductDetails.

6. MainViewModel

Najtrudniejsze masz za sobą. Teraz określisz interfejs MainViewModel, który jest tylko publicznym interfejsem Twoich klientów, więc nie będą oni musieli znać się na funkcjach BillingClientWrapper i SubscriptionDataRepository.

Najpierw w MainViewModel rozpoczynamy połączenie rozliczeniowe po zainicjowaniu modelu viewModel.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Następnie przepływy z repozytorium są łączone odpowiednio w productsForSaleFlows (w przypadku dostępnych usług) i userCurrentSubscriptionFlow (w przypadku bieżącej i aktywnej subskrypcji użytkownika) zgodnie z przetwarzaniem w klasie repozytorium.

Lista bieżących zakupów jest również dostępna w interfejsie użytkownika currentPurchasesFlow.

MainViewModel.kt

val productsForSaleFlows = combine(
   repo.basicProductDetails,
   repo.premiumProductDetails
) { basicProductDetails,
   premiumProductDetails
   ->
   MainState(
       basicProductDetails = basicProductDetails,
       premiumProductDetails = premiumProductDetails
   )
}

// The userCurrentSubscriptionFlow object combines all the possible subscription flows into one
// for emission.
private val userCurrentSubscriptionFlow = combine(
   repo.hasRenewableBasic,
   repo.hasPrepaidBasic,
   repo.hasRenewablePremium,
   repo.hasPrepaidPremium
) { hasRenewableBasic,
   hasPrepaidBasic,
   hasRenewablePremium,
   hasPrepaidPremium
   ->
   MainState(
       hasRenewableBasic = hasRenewableBasic,
       hasPrepaidBasic = hasPrepaidBasic,
       hasRenewablePremium = hasRenewablePremium,
       hasPrepaidPremium = hasPrepaidPremium
   )
}

// Current purchases.
val currentPurchasesFlow = repo.purchases

Połączona wartość userCurrentSubscriptionFlow jest gromadzona w bloku inicjowania, a wartość jest przesyłana do obiektu MutableLiveData o nazwie _destinationScreen.

init {
   viewModelScope.launch {
       userCurrentSubscriptionFlow.collectLatest { collectedSubscriptions ->
           when {
               collectedSubscriptions.hasRenewableBasic == true &&
                       collectedSubscriptions.hasRenewablePremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasRenewablePremium == true &&
                       collectedSubscriptions.hasRenewableBasic == false -> {
                   _destinationScreen.postValue(DestinationScreen.PREMIUM_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasPrepaidBasic == true &&
                       collectedSubscriptions.hasPrepaidPremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN)
               }
               collectedSubscriptions.hasPrepaidPremium == true &&
                       collectedSubscriptions.hasPrepaidBasic == false -> {
                   _destinationScreen.postValue(
                       DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN
                   )
               }
               else -> {
                   _destinationScreen.postValue(DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN)
               }
           }
       }

   }
}

MainViewModel dodaje też kilka bardzo przydatnych metod:

  1. Pobieranie abonamentów podstawowych i tokenów ofert

Od Biblioteki płatności w Play w wersji 5.0.0 wszystkie usługi objęte subskrypcją mogą mieć wiele abonamentów podstawowych i ofert. Nie dotyczy to przedpłaconych abonamentów podstawowych, które nie mogą mieć ofert.

Ta metoda ułatwia pobieranie wszystkich ofert i abonamentów podstawowych, do których może mieć dostęp użytkownik, dzięki nowej koncepcji tagów, które służą do grupowania powiązanych ofert.

Jeśli na przykład użytkownik spróbuje kupić miesięczną subskrypcję podstawową, wszystkie abonamenty podstawowe i oferty powiązane z miesięczną subskrypcją podstawową zostaną oznaczone ciągiem znaków monthlyBasic.

MainViewModel.kt

private fun retrieveEligibleOffers(
   offerDetails: MutableList<ProductDetails.SubscriptionOfferDetails>,
   tag: String
): List<ProductDetails.SubscriptionOfferDetails> {
   val eligibleOffers = emptyList<ProductDetails.SubscriptionOfferDetails>().toMutableList()
   offerDetails.forEach { offerDetail ->
       if (offerDetail.offerTags.contains(tag)) {
           eligibleOffers.add(offerDetail)
       }
   }

   return eligibleOffers
}
  1. Obliczanie najtańszej oferty

Gdy użytkownik kwalifikuje się do kilku ofert, do obliczenia najniższej oferty spośród ofert zwróconych przez retrieveEligibleOffers() używana jest metoda leastPricedOfferToken().

Ta metoda zwraca token identyfikatora wybranej oferty.

Ta implementacja zwraca najniższe oferty w ramach zestawu pricingPhases i nie uwzględnia średnich.

Inną implementacją może być oferta najniższej średniej ceny.

MainViewModel.kt

private fun leastPricedOfferToken(
   offerDetails: List<ProductDetails.SubscriptionOfferDetails>
): String {
   var offerToken = String()
   var leastPricedOffer: ProductDetails.SubscriptionOfferDetails
   var lowestPrice = Int.MAX_VALUE

   if (!offerDetails.isNullOrEmpty()) {
       for (offer in offerDetails) {
           for (price in offer.pricingPhases.pricingPhaseList) {
               if (price.priceAmountMicros < lowestPrice) {
                   lowestPrice = price.priceAmountMicros.toInt()
                   leastPricedOffer = offer
                   offerToken = leastPricedOffer.offerToken
               }
           }
       }
   }
   return offerToken
}
  1. Kreatory BillingFlowParams

Aby rozpocząć proces zakupu konkretnego produktu, ProductDetails i wybrany token oferty muszą być ustawione i użyte do utworzenia parametru BilingFlowParams.

Możesz to zrobić na 2 sposoby:

upDowngradeBillingFlowParamsBuilder() kompiluje parametry przejścia na wyższą i niższą wersję.

MainViewModel.kt

private fun upDowngradeBillingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String,
   oldToken: String
): BillingFlowParams {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   ).setSubscriptionUpdateParams(
       BillingFlowParams.SubscriptionUpdateParams.newBuilder()
           .setOldPurchaseToken(oldToken)
           .setReplaceProrationMode(
               BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
           )
           .build()
   ).build()
}

billingFlowParamsBuilder() tworzy parametry dla zwykłych zakupów.

MainViewModel.kt

private fun billingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String
): BillingFlowParams.Builder {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   )
}
  1. Metoda zakupu

Metoda zakupu do uruchamiania zakupów wykorzystuje launchBillingFlow() i BillingFlowParams usługi BillingClientWrapper.

MainViewModel.kt

fun buy(
   productDetails: ProductDetails,
   currentPurchases: List<Purchase>?,
   activity: Activity,
   tag: String
) {
   val offers =
       productDetails.subscriptionOfferDetails?.let {
           retrieveEligibleOffers(
               offerDetails = it,
               tag = tag.lowercase()
           )
       }
   val offerToken = offers?.let { leastPricedOfferToken(it) }
   val oldPurchaseToken: String

   // Get current purchase. In this app, a user can only have one current purchase at
   // any given time.
   if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size == MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // This either an upgrade, downgrade, or conversion purchase.
       val currentPurchase = currentPurchases.first()

       // Get the token from current purchase.
       oldPurchaseToken = currentPurchase.purchaseToken

       val billingParams = offerToken?.let {
           upDowngradeBillingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it,
               oldToken = oldPurchaseToken
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams
           )
       }
   } else if (currentPurchases == null) {
       // This is a normal purchase.
       val billingParams = offerToken?.let {
           billingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams.build()
           )
       }
   } else if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size > MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // The developer has allowed users  to have more than 1 purchase, so they need to
       /// implement a logic to find which one to use.
       Log.d(TAG, "User has more than 1 current purchase.")
   }
}

d726a27add092140.png

  1. Zakończenie połączenia z rozliczeniem

Na koniec metoda terminateBillingConnection BillingClientWrapper jest wywoływana z obiektu onCleared() modelu widoku danych.

Ma to na celu zakończenie bieżącego połączenia rozliczeniowego po zniszczeniu powiązanej aktywności.

7. Interfejs

Nadszedł czas, by zacząć korzystać ze wszystkich funkcji interfejsu użytkownika. W tym celu będziesz pracować z klasami Composables i MainActivity.

Composables.kt

Klasa Composables jest w pełni dostarczona i definiuje wszystkie funkcje Compose używane do renderowania interfejsu użytkownika i mechanizmu nawigacji między nimi.

Funkcja Subscriptions wyświetla 2 przyciski: Basic Subscription i Premium Subscription.

Basic Subscription i Premium Subscription ładują nowe metody tworzenia wiadomości, które wyświetlają 3 abonamenty podstawowe: miesięczny, roczny i przedpłacony.

W przypadku konkretnej subskrypcji użytkownik może też mieć 3 funkcje tworzenia profilu: odnawialny podstawowy, odnawialny profil Premium oraz przedpłacony profil podstawowy lub przedpłacony Premium.

  • Użytkownicy z subskrypcją Basic umożliwia mu przejście na miesięczną, roczną lub przedpłaconą subskrypcję premium.
  • Jeśli użytkownik ma subskrypcję Premium, może przejść na subskrypcję miesięczną, roczną lub przedpłaconą.
  • Jeśli użytkownik ma subskrypcję przedpłaconą, może ją przedłużyć za pomocą przycisku doładowania lub zamienić swoją subskrypcję przedpłaconą na odpowiedni automatycznie odnawiany abonament podstawowy.

Dostępna jest też funkcja wczytywania ekranu, która jest używana podczas nawiązywania połączenia z Google Play i renderowania profilu użytkownika.

MainActivity.kt

  1. Po utworzeniu obiektu MainActivity tworzone jest wystąpienie modelu viewModel i ładowana funkcja tworzenia wiadomości o nazwie MainNavHost.
  2. MainNavHost zaczyna się od zmiennej isBillingConnected utworzonej na podstawie aktywnych danych billingConnectionSate modelu viewModel i jest monitorowana pod kątem zmian, ponieważ po utworzeniu wystąpienia modelu vieModel przekazuje on billingConnectionSate do metody startBillingConnection BillingClientWrapper

isBillingConnected ma wartość „true” (prawda), gdy połączenie jest nawiązane, i na wartość „false” (fałsz), gdy nie jest włączone.

Jeśli zasada ma wartość Fałsz, funkcja tworzenia wiadomości LoadingScreen() jest ładowana, a jeśli ma wartość Prawda, są ładowane funkcje Subscription i profilu.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Po nawiązaniu połączenia z płatnościami:

Utworzono wystąpienie navController

val navController = rememberNavController()

Następnie zbierane są przepływy w MainViewModel.

val productsForSale by viewModel.productsForSaleFlows.collectAsState(
   initial = MainState()
)

val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
   initial = listOf()
)

Na koniec obserwowana jest zmienna LiveData destinationScreen modelu viewModel.

Na podstawie bieżącego stanu subskrypcji użytkownika renderowana jest odpowiednia funkcja tworzenia wiadomości.

val screen by viewModel.destinationScreen.observeAsState()
when (screen) {
   // User has a Basic Prepaid subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_monthly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_yearly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_BASIC_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable basic subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_RENEWABLE_PROFILE -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.monthly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.basic_sub_message
       )
   }
   // User has a prepaid Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_monthly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_yearly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_PREMIUM_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_RENEWABLE_PROFILE -> {
       UserProfile(
           listOf(
               ButtonModel(R.string.monthly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.premium_sub_message
       )
   }
   // User has no current subscription - the subscription composable
   // is loaded.
   MainViewModel.DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN -> {
       SubscriptionNavigationComponent(
           productsForSale = productsForSale,
           navController = navController,
           viewModel = viewModel
       )
   }
}

8. Kod rozwiązania

Pełny kod rozwiązania znajdziesz w module rozwiązania.

9. Gratulacje

Gratulujemy! Udało Ci się zintegrować usługi subskrypcji Biblioteki płatności w Google Play w wersji 5.0.0 w bardzo prostej aplikacji.

Udokumentowaną wersję bardziej zaawansowanej aplikacji z konfiguracją bezpiecznego serwera znajdziesz tutaj.

Więcej informacji