Vendere abbonamenti nell'app con Libreria Fatturazione Play 5

1. Introduzione

Il sistema di fatturazione di Google Play è un servizio che ti consente di vendere contenuti e prodotti digitali nella tua app per Android. È il modo più diretto per vendere prodotti in-app per monetizzare la tua app. Questo codelab ti mostra come utilizzare la Libreria Fatturazione Google Play per vendere abbonamenti nel tuo progetto in modo da integrare tutti i dettagli essenziali durante l'integrazione degli acquisti con il resto della tua app.

Inoltre, introduce i concetti relativi agli abbonamenti, come piani base, offerte, tag e piani prepagati. Per scoprire di più sugli abbonamenti relativi al servizio Fatturazione Google Play, puoi visitare il nostro Centro assistenza.

Cosa creerai

In questo codelab, aggiungerai la libreria di fatturazione più recente (versione 5.0.0) a una semplice app di profilo basata su abbonamento. L'app è già stata creata per te, quindi aggiungerai solo la parte relativa alla fatturazione. Come mostrato nella Figura 1, in questa app l'utente sottoscrive uno qualsiasi dei piani base e/o offerte offerti tramite due prodotti in abbonamento rinnovabili (base e premium) o per un pagamento prepagato non rinnovabile. È tutto. I piani base sono rispettivamente abbonamenti mensili e annuali. L'utente può eseguire l'upgrade, il downgrade o convertire un abbonamento prepagato a un abbonamento rinnovabile.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

Per incorporare la Libreria Fatturazione Google Play nella tua app, devi creare quanto segue:

  • BillingClientWrapper: un wrapper per la libreria BillingClient. Ha lo scopo di incapsulare le interazioni con il Client di fatturazione della Libreria Fatturazione Play, ma non è obbligatorio nella tua integrazione.
  • SubscriptionDataRepository: un repository di fatturazione per la tua app, che contiene un elenco dell'inventario dei prodotti in abbonamento dell'app (ovvero i prodotti in vendita) e un elenco di variabili ShareFlow che consente di raccogliere lo stato degli acquisti e i dettagli dei prodotti.
  • MainViewModel: un ViewModel attraverso il quale il resto dell'app comunica con il repository di fatturazione. Consente di avviare il flusso di fatturazione nell'interfaccia utente utilizzando vari metodi di acquisto.

Al termine, l'architettura della tua app dovrebbe essere simile alla figura seguente:

c83bc759f32b0a63.png

Cosa imparerai a fare

  • Come integrare la Libreria Fatturazione Play
  • Come creare prodotti in abbonamento, piani base, offerte e tag tramite Play Console
  • Come recuperare dall'app i piani base e le offerte disponibili
  • Come avviare il flusso di fatturazione con i parametri appropriati
  • Come offrire prodotti in abbonamento prepagati

Questo codelab è incentrato sulla fatturazione di Google Play. Concetti e blocchi di codice non pertinenti sono trattati solo superficialmente e sono forniti solo per operazioni di copia e incolla.

Che cosa ti serve

  • Una versione recente di Android Studio (>= Arctic Fox | 2020.3.1)
  • Un dispositivo con Android 8.0 o versioni successive.
  • Il codice campione, fornito su GitHub (istruzioni nelle sezioni successive)
  • Conoscenza moderata dello sviluppo Android su Android Studio
  • Conoscere la pubblicazione di un'app sul Google Play Store
  • Esperienza moderata nella scrittura di codice Kotlin
  • Libreria Fatturazione Google Play versione 5.0.0

2. Preparazione

Ottieni il codice da GitHub

Abbiamo inserito tutto il necessario per questo progetto in un repository Git. Per iniziare, devi recuperare il codice e aprirlo nel tuo ambiente di sviluppo preferito. Per questo codelab, ti consigliamo di utilizzare Android Studio.

Il codice per iniziare è archiviato in un repository GitHub. Puoi clonare il repository con il seguente comando:

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

3. Le basi

Qual è il nostro punto di partenza?

Il nostro punto di partenza è un'app di profilo utente di base progettata per questo codelab. Il codice è stato semplificato per mostrare i concetti da illustrare e non è stato progettato per l'uso in produzione. Se scegli di riutilizzare qualsiasi parte di questo codice in un'app di produzione, assicurati di seguire le best practice e di testare tutto il codice.

Importa il progetto in Android Studio

PlayBillingCodelab è l'app di base che non contiene un'implementazione della Fatturazione Google Play. Avvia Android Studio e importa il codelab di fatturazione scegliendo Open > billing/PlayBillingCodelab

Il progetto è formato da due moduli:

  • start ha l'app scheletro, ma non dispone delle dipendenze richieste e di tutti i metodi da implementare.
  • finited contiene il progetto completato e può fungere da guida quando non riesci a procedere.

L'app è composta da otto file del corso: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory e MainActivity.

  • BillingClientWrapper è un wrapper che isola i metodi [BillingClient] di Fatturazione Google Play necessari per una semplice implementazione e invia risposte al repository di dati per l'elaborazione.
  • SubscriptionDataRepository viene utilizzato per astrarre l'origine dati di fatturazione di Google Play (ovvero la libreria client di fatturazione) e converte i dati StateFlow emessi in BillingClientWrapper in flussi.
  • ButtonModel è una classe di dati utilizzata per creare pulsanti nella UI.
  • Composables estrae tutti i metodi componibili dell'interfaccia utente in un'unica classe.
  • MainState è una classe di dati per la gestione dello stato.
  • MainViewModel viene utilizzato per conservare i dati e gli stati relativi alla fatturazione utilizzati nell'interfaccia utente. Combina tutti i flussi in SubscriptionDataRepository in un unico oggetto di stato.
  • MainActivity è la classe di attività principale che carica i componenti componibili per l'interfaccia utente.
  • Costanti è l'oggetto che contiene le costanti utilizzate da più classi.

Gradle

Devi aggiungere una dipendenza Gradle per poter aggiungere Fatturazione Google Play alla tua app. Apri il file build.gradle del modulo dell'app e aggiungi quanto segue:

dependencies {
    val billing_version = "5.0.0"

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

Google Play Console

Ai fini di questo codelab, devi creare le due seguenti offerte di prodotti in abbonamento nella sezione degli abbonamenti di Google Play Console:

  • 1 abbonamento di base con ID prodotto up_basic_sub

Il prodotto deve avere 3 piani base (2 con rinnovo automatico e 1 prepagato) con tag associati : un abbonamento base mensile con il tag monthlybasic, un abbonamento base annuale con il tag yearlybasic e un abbonamento prepagato con il tag prepaidbasic.

Puoi aggiungere offerte ai piani base. Le offerte erediteranno i tag dai piani base associati.

  • 1 abbonamento Premium con ID prodotto up_premium_sub

Il prodotto deve avere 3 piani base(2 con rinnovo automatico e 1 prepagato) con tag associati: 1 abbonamento base mensile con il tag monthlypremium, 1 abbonamento base annuale con il tag yearlypremium e 1 abbonamento prepagato con il tag prepaidpremium.

a9f6fd6e70e69fed.png

Puoi aggiungere offerte ai piani base. Le offerte erediteranno i tag dai piani base associati.

Per informazioni più dettagliate su come creare prodotti in abbonamento, piani base, offerte e tag, visita il Centro assistenza Google Play.

4. Il cliente di fatturazione ha configurato

Per questa sezione, lavorerai nel corso BillingClientWrapper.

Al termine, avrai tutto il necessario per creare l'istanza del cliente di fatturazione e tutti i metodi correlati.

  1. Inizializzare un client di fatturazione

Dopo aver aggiunto una dipendenza alla Libreria Fatturazione Google Play, dobbiamo inizializzare un'istanza BillingClient.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Stabilire una connessione a Google Play

Dopo aver creato un Client di fatturazione, dobbiamo stabilire una connessione a Google Play.

Per la connessione a Google Play, chiamiamo startConnection(). Il processo di connessione è asincrono e dobbiamo implementare un'BillingClientStateListener per ricevere un callback quando la configurazione del client è stata completata ed è pronto a effettuare ulteriori richieste.

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. Eseguire query sulla fatturazione di Google Play per gli acquisti esistenti

Dopo aver stabilito un collegamento con Google Play, possiamo inviare query relative agli acquisti effettuati in precedenza dall'utente chiamando il numero 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. Mostrare i prodotti disponibili per l'acquisto

Ora possiamo eseguire query sui prodotti disponibili e mostrarli agli utenti. Per richiedere su Google Play i dettagli dei prodotti in abbonamento, chiameremo queryProductDetailsAsync(). Eseguire query sui dettagli dei prodotti è un passaggio importante prima di mostrare i prodotti agli utenti, poiché restituisce informazioni localizzate sui prodotti.

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. Impostare il listener per la query ProductDetails

Nota: questo metodo invia il risultato della query in una mappa a _productWithProductDetails.

Tieni inoltre presente che la query dovrebbe restituire ProductDetails. Se ciò non dovesse verificarsi, è molto probabile che il problema sia dovuto al fatto che i prodotti configurati in Play Console non siano stati attivati o che tu non abbia pubblicato una build con la dipendenza del client di fatturazione in nessuno dei canali di release.

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. Lancia il flusso di acquisto

launchBillingFlow è il metodo chiamato quando l'utente fa clic per acquistare un articolo. Chiedi a Google Play di avviare il flusso di acquisto con il ProductDetails del prodotto.

BillingClientWrapper.kt

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

}
  1. Imposta il listener per il risultato dell'operazione di acquisto

Quando l'utente esce dalla schermata di acquisto su Google Play (toccando il pulsante "Acquista" per completare l'acquisto o toccando il pulsante Indietro per annullare l'acquisto), il callback onPurchaseUpdated() invia il risultato del flusso di acquisto alla tua app. In base ai BillingResult.responseCode, puoi quindi determinare se l'utente ha acquistato il prodotto correttamente. Se responseCode == OK, significa che l'acquisto è stato completato correttamente.

onPurchaseUpdated() restituisce un elenco di Purchase oggetti che include tutti gli acquisti che l'utente ha effettuato tramite l'app. Tra i tanti altri campi, ogni oggetto Purchase contiene gli attributi ID prodotto, purchaseToken e isAcknowledged. Utilizzando questi campi, per ogni oggetto Purchase puoi quindi determinare se si tratta di un nuovo acquisto da elaborare o di un acquisto esistente che non richiede ulteriori elaborazioni.

Per gli acquisti di abbonamenti, l'elaborazione equivale all'accettazione del nuovo acquisto.

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. Elaborazione degli acquisti (verifica e conferma degli acquisti)

Una volta che un utente completa un acquisto, l'app deve elaborare l'acquisto confermandolo.

Inoltre, il valore di _isNewPurchaseAcknowledged viene impostato su true quando la conferma viene elaborata correttamente.

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. Termina connessione di fatturazione

Infine, quando un'attività viene eliminata, vuoi interrompere la connessione a Google Play, in modo che endConnection() venga chiamato a farlo.

BillingClientWrapper.kt

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

5. SubscriptionDataRepository

Nel BillingClientWrapper, le risposte di QueryPurchasesAsync e QueryProductDetails vengono pubblicate rispettivamente su MutableStateFlow _purchases e _productWithProductDetails che sono esposte al di fuori del corso con acquisti e productWithProductDetails.

In SubscriptionDataRepository, gli acquisti vengono elaborati in tre flussi basati sul prodotto dell'acquisto restituito: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium e hasPremiumPrepaid.

Inoltre, productWithProductDetails viene elaborato nei rispettivi flussi basicProductDetails e premiumProductDetails.

6. MainViewModel

La parte difficile è finita. Ora definisci MainViewModel, che è solo un'interfaccia pubblica per i tuoi clienti che non devono necessariamente conoscere i componenti interni di BillingClientWrapper e SubscriptionDataRepository.

Innanzitutto, in MainViewModel, iniziamo la connessione di fatturazione quando il viewModel viene inizializzato.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Quindi, i flussi del repository vengono combinati rispettivamente in productsForSaleFlows (per i prodotti disponibili) e userCurrentSubscriptionFlow (per l'abbonamento attuale e attivo dell'utente) come elaborati nella classe repo.

L'elenco degli acquisti attuali viene inoltre reso disponibile nell'interfaccia utente con 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

L'elemento userCurrentSubscriptionFlow combinato viene raccolto in un blocco di inizializzazione e il valore viene pubblicato in un oggetto MutableLiveData denominato _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 aggiunge anche alcuni metodi molto utili:

  1. Recupero dei token Base e Offer

A partire dalla versione 5.0.0 di Libreria Fatturazione Play, tutti i prodotti in abbonamento possono avere più piani base e offerte, ad eccezione dei piani base prepagati che non possono avere offerte.

Questo metodo consente di recuperare tutte le offerte e i piani base a cui un utente è idoneo utilizzando il nuovo concetto di tag utilizzati per raggruppare le offerte correlate.

Ad esempio, quando un utente tenta di acquistare un abbonamento di base mensile, tutti i piani base e le offerte associate al prodotto in abbonamento di base mensile vengono contrassegnati con la stringa 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. Calcolo dell'offerta con il prezzo più basso

Quando un utente è idoneo per più offerte, viene utilizzato il metodo leastPricedOfferToken() per calcolare l'offerta più bassa tra quelle restituite da retrieveEligibleOffers().

Il metodo restituisce il token ID offerta dell'offerta selezionata.

Questa implementazione restituisce semplicemente le offerte con il prezzo più basso in termini di pricingPhases impostato e non tiene conto delle medie.

Un'altra implementazione potrebbe essere l'offerta dal prezzo medio più basso.

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. Creatori di BillingFlowParams

Per avviare il flusso di acquisto di un determinato prodotto, quest'ultimo Il valore ProductDetails e il token dell'offerta selezionata devono essere impostati e utilizzati per creare un parametro BilingFlowParams.

Esistono due metodi per risolvere questo problema:

upDowngradeBillingFlowParamsBuilder() crea i parametri per gli upgrade e i downgrade.

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() crea i parametri per i normali acquisti.

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. Metodo di acquisto

Il metodo di acquisto utilizza launchBillingFlow() di BillingClientWrapper e BillingFlowParams per avviare gli acquisti.

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. Terminare la connessione di fatturazione

Infine, il metodo terminateBillingConnection di BillingClientWrapper viene richiamato su onCleared() di un ViewModel.

Questo serve a terminare la connessione di fatturazione attuale quando l'attività associata viene eliminata.

7. UI

A questo punto, puoi utilizzare tutto ciò che hai creato nell'interfaccia utente. Per aiutarti, lavorerai con le classi Composables e MainActivity.

Composables.kt

La classe Composables è completamente fornita e definisce tutte le funzioni Compose utilizzate per eseguire il rendering della UI e del meccanismo di navigazione tra di esse.

La funzione Subscriptions mostra due pulsanti: Basic Subscription e Premium Subscription.

Basic Subscription e Premium Subscription caricano ciascuno nuovi metodi Compose che mostrano i tre rispettivi piani base: mensile, annuale e prepagato.

Quindi, ci sono tre possibili funzioni di composizione del profilo, ciascuna per uno specifico abbonamento che un utente può avere: un profilo Basic rinnovabile, un Premium rinnovabile e un profilo prepagato Basic o prepagato Premium.

  • Se un utente dispone di un abbonamento Premium, il profilo di base consente di eseguire l'upgrade a un abbonamento Premium mensile, annuale o prepagato.
  • Al contrario, se un utente dispone di un abbonamento Premium, può eseguire il downgrade a un abbonamento mensile, annuale o prepagato di base.
  • Se un utente ha un abbonamento prepagato, può ricaricare l'abbonamento con il pulsante di ricarica o convertire l'abbonamento prepagato in un piano base con rinnovo automatico corrispondente.

Infine, c'è una funzione di caricamento della schermata che viene utilizzata quando viene stabilita la connessione a Google Play e viene eseguito il rendering di un profilo utente.

MainActivity.kt

  1. Quando viene creato MainActivity, viene creata l'istanza di viewModel e viene caricata una funzione di composizione denominata MainNavHost.
  2. MainNavHost inizia con una variabile isBillingConnected creata dai dati Livedata billingConnectionSate di viewModel e osservata per le modifiche perché, quando viene creata un'istanza di vieModel, passa billingConnectionSate al metodo startBillingConnection di BillingClientWrapper.

isBillingConnected viene impostato su true se viene stabilita la connessione e su false in caso contrario.

Se il valore è false, vengono caricate la funzione di composizione LoadingScreen() e, se true, vengono caricate le funzioni Subscription o profilo.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Quando viene stabilita una connessione per la fatturazione:

Viene creata un'istanza di Compose navController

val navController = rememberNavController()

Quindi, vengono raccolti i flussi in MainViewModel.

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

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

Infine, viene osservata la variabile LiveData destinationScreen di viewModel.

In base allo stato attuale dell'abbonamento dell'utente, viene eseguito il rendering della funzione di scrittura corrispondente.

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. Codice della soluzione

Il codice completo della soluzione è disponibile nel relativo modulo.

9. Complimenti

Congratulazioni, hai integrato correttamente i prodotti in abbonamento a Libreria Fatturazione Google Play 5.0.0 in un'app molto semplice.

Per la versione documentata di un'app più sofisticata con una configurazione sicura del server, vedi l'esempio ufficiale.

Per approfondire