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ą.
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ówMainViewModel
– 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:
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 pozwala 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 Composables 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
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.
- 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()
- 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)
}
})
}
- 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)
}
}
}
- 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)
}
}
}
- 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")
}
}
}
- 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)
}
- 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.
}
}
- 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
}
}
}
}
}
- 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:
- 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
}
- 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
}
- 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()
)
)
}
- 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.")
}
}
- 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
- Po utworzeniu obiektu
MainActivity
tworzone jest wystąpienie modelu viewModel i ładowana funkcja tworzenia wiadomości o nazwieMainNavHost
. MainNavHost
zaczyna się od zmiennejisBillingConnected
utworzonej na podstawie aktywnych danychbillingConnectionSate
modelu viewModel i jest monitorowana pod kątem zmian, ponieważ po utworzeniu wystąpienia modelu vieModel przekazuje onbillingConnectionSate
do metody startBillingConnectionBillingClientWrapper
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()
- 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 wymagające 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.