Vendre des abonnements dans une application avec la bibliothèque Play Billing 5

1. Introduction

Le système de facturation de Google Play est un service qui vous permet de vendre des produits et des contenus numériques dans votre application Android. Il s'agit du moyen le plus direct de vendre des produits intégrés pour monétiser votre application. Cet atelier de programmation vous explique comment utiliser la bibliothèque Google Play Billing pour vendre des abonnements dans votre projet en encapsulant tous les détails lorsque vous intégrez les achats au reste de votre application.

Il présente également des concepts liés aux abonnements, comme les forfaits de base, les offres, les tags et les forfaits prépayés. Pour en savoir plus sur les abonnements Google Play Billing, consultez notre Centre d'aide.

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez ajouter la dernière bibliothèque de facturation (version 5.0.0) à une application de profil simple basée sur un abonnement. L'application a déjà été conçue pour vous, vous n'allez donc ajouter que la partie facturation. Comme le montre la figure 1, dans cette application, l'utilisateur souscrit à l'un des forfaits de base et/ou aux offres proposés via deux abonnements renouvelables (de base et premium) ou à un forfait prépayé non renouvelable. C'est tout. Les forfaits de base correspondent respectivement à des abonnements mensuels et annuels. L'utilisateur peut passer à un forfait supérieur ou inférieur, ou passer d'un abonnement prépayé à un abonnement renouvelable.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

Pour intégrer la bibliothèque Google Play Billing à votre application, vous devez créer les éléments suivants:

  • BillingClientWrapper : wrapper pour la bibliothèque BillingClient Il vise à encapsuler les interactions avec le BillingClient de la bibliothèque Play Billing, mais il n'est pas requis dans votre propre intégration.
  • SubscriptionDataRepository : un référentiel de facturation pour votre application qui contient une liste de l'inventaire des produits sur abonnement de l'application (c'est-à-dire ce qui est à vendre), ainsi qu'une liste de variables ShareFlow qui permet de recueillir l'état des achats et les informations détaillées sur les produits
  • MainViewModel : ViewModel via lequel le reste de votre application communique avec le dépôt de facturation. Il est utile de lancer le flux de facturation dans l'interface utilisateur à l'aide de différentes méthodes d'achat.

Une fois l'opération terminée, l'architecture de votre application doit ressembler à la figure ci-dessous:

c83bc759f32b0a63.png

Points abordés

  • Intégrer la bibliothèque Play Billing
  • Créer des produits sur abonnement, des forfaits de base, des offres et des tags via la Play Console
  • Récupérer les forfaits de base et les offres disponibles dans l'application
  • Lancer le flux de facturation avec les paramètres appropriés
  • Proposer des produits sur abonnement prépayé

Cet atelier de programmation est consacré à Google Play Billing. Les concepts et les blocs de codes non pertinents ne sont pas abordés, et vous sont fournis afin que vous puissiez simplement les copier et les coller.

Prérequis

  • Une version récente d'Android Studio (>= Arctic Fox | 2020.3.1)
  • Un appareil Android équipé d'Android 8.0 ou version ultérieure
  • L'exemple de code fourni sur GitHub (instructions dans les sections suivantes)
  • Connaissance moyenne du développement Android sur Android Studio
  • Vous disposez des connaissances nécessaires pour publier une application sur le Google Play Store.
  • Expérience moyenne en écriture de code Kotlin
  • Bibliothèque Google Play Billing version 5.0.0

2. Configuration

Obtenir le code depuis GitHub

Pour ce projet, nous avons regroupé tout ce dont vous avez besoin dans un dépôt Git. Pour commencer, vous devez récupérer le code et l'ouvrir dans votre environnement de développement préféré. Pour cet atelier de programmation, nous vous recommandons d'utiliser Android Studio.

Le code pour commencer est stocké dans un dépôt GitHub. Vous pouvez cloner le dépôt à l'aide de la commande suivante:

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

3. Les fondations

Quel est notre point de départ ?

Notre point de départ est une application de profil utilisateur de base conçue pour cet atelier de programmation. Le code a été simplifié afin de montrer les concepts que nous voulons illustrer, et il n'a pas été conçu pour une utilisation en production. Si vous choisissez de réutiliser une partie de ce code dans une application de production, veillez à suivre les bonnes pratiques et à tester entièrement l'intégralité de votre code.

Importer le projet dans Android Studio

PlayBillingCodelab est l'application de base qui ne contient pas d'implémentation de Google Play Billing. Démarrez Android Studio et importez Billing-Codelab en sélectionnant Open > billing/PlayBillingCodelab.

Le projet comporte deux modules :

  • start dispose de l'application squelette, mais ne dispose pas des dépendances requises ni de toutes les méthodes à implémenter.
  • finished dispose du projet terminé et peut vous servir de guide lorsque vous êtes bloqué.

L'application se compose de huit fichiers de classe: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory et MainActivity.

  • BillingClientWrapper est un wrapper qui isole les méthodes [BillingClient] de Google Play Billing nécessaires à une implémentation simple et émet des réponses dans le dépôt de données à des fins de traitement.
  • SubscriptionDataRepository permet d'extraire la source de données Google Play Billing (c'est-à-dire la bibliothèque du client de facturation) et de convertir les données StateFlow émises dans BillingClientWrapper en flux.
  • ButtonModel est une classe de données utilisée pour créer des boutons dans l'interface utilisateur.
  • Composables extrait toutes les méthodes composables de l'UI en une seule classe.
  • MainState est une classe de données permettant de gérer les états.
  • MainViewModel permet de conserver les états et les données liés à la facturation utilisés dans l'UI. Elle combine tous les flux d'SubscriptionDataRepository en un seul objet d'état.
  • MainActivity est la classe d'activité principale qui charge les composables pour l'interface utilisateur.
  • Constantes est l'objet qui contient les constantes utilisées par plusieurs classes.

Gradle

Vous devez ajouter une dépendance Gradle pour ajouter Google Play Billing à votre application. Ouvrez le fichier build.gradle du module d'application et ajoutez les éléments suivants:

dependencies {
    val billing_version = "5.0.0"

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

Google Play Console

Pour les besoins de cet atelier de programmation, vous devez créer les deux offres de produits sur abonnement suivantes dans la section Abonnements de la Google Play Console:

  • 1 abonnement de base avec l'ID produit up_basic_sub

Le produit doit être associé à trois forfaits de base (deux à renouvellement automatique et un prépayé) avec les balises associées : un abonnement de base mensuel avec la balise monthlybasic, un abonnement de base annuel avec la balise yearlybasic et un abonnement prépayé avec la balise prepaidbasic

Vous pouvez ajouter des offres aux forfaits de base. Les offres hériteront des tags des forfaits de base associés.

  • 1 abonnement Premium avec l'ID produit up_premium_sub

Le produit doit être associé à trois forfaits de base(deux à renouvellement automatique et un prépayé) avec les balises associées: un abonnement de base mensuel avec la balise monthlypremium, un abonnement de base annuel avec la balise yearlypremium et un abonnement prépayé avec la balise prepaidpremium

a9f6fd6e70e69fed.png

Vous pouvez ajouter des offres aux forfaits de base. Les offres hériteront des tags des forfaits de base associés.

Pour en savoir plus sur la création de produits sur abonnement, de forfaits de base, d'offres et de tags, consultez le Centre d'aide Google Play.

4. Configuration du client de facturation

Pour cette section, vous travaillerez dans la classe BillingClientWrapper.

À la fin, vous aurez tout ce dont vous avez besoin pour instancier le client de facturation, ainsi que toutes les méthodes associées.

  1. Initialiser un BillingClient

Une fois que nous avons ajouté une dépendance à la bibliothèque Google Play Billing, nous devons initialiser une instance BillingClient.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Établir une connexion avec Google Play

Une fois que vous avez créé un BillingClient, nous devons établir une connexion avec Google Play.

Pour associer Google Play, nous appelons startConnection(). Le processus de connexion est asynchrone. Nous devons implémenter un BillingClientStateListener pour recevoir un rappel une fois que la configuration du client est terminée et qu'il est prêt à envoyer d'autres requêtes.

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. Interroger Google Play Billing pour les achats existants

Une fois la connexion à Google Play établie, nous pouvons interroger les achats que l'utilisateur a précédemment effectués en appelant 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. Afficher les produits disponibles à l'achat

Nous pouvons maintenant rechercher les produits disponibles et les présenter aux utilisateurs. Pour interroger Google Play afin d'obtenir des informations sur un produit sur abonnement, nous appellerons queryProductDetailsAsync(). L'interrogation des informations détaillées sur les produits est une étape importante avant de présenter les produits aux utilisateurs, car elle renvoie des informations localisées sur les produits.

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. Définir l'écouteur pour la requête ProductDetails

Remarque: Cette méthode émet le résultat de la requête dans un élément Map vers _productWithProductDetails.

Notez également que la requête doit renvoyer ProductDetails. Si ce n'est pas le cas, le problème est probablement dû au fait que les produits configurés dans la Play Console n'ont probablement pas été activés ou que vous n'avez publié aucun build avec la dépendance du client de facturation dans les canaux de publication.

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. Lancer le parcours d'achat

launchBillingFlow est la méthode appelée lorsque l'utilisateur clique pour acheter un article. Google Play lance le parcours d'achat avec l'ProductDetails du produit.

BillingClientWrapper.kt

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

}
  1. Définir l'écouteur pour le résultat de l'opération d'achat

Lorsque l'utilisateur quitte l'écran d'achat Google Play (en appuyant sur le bouton "Acheter" pour finaliser l'achat ou en appuyant sur le bouton "Retour" pour annuler l'achat), le rappel onPurchaseUpdated() renvoie le résultat du parcours d'achat à votre application. En vous basant sur la BillingResult.responseCode, vous pouvez déterminer si l'utilisateur a bien acheté le produit. Si la valeur est responseCode == OK, cela signifie que l'achat a bien été effectué.

onPurchaseUpdated() renvoie une liste d'objets Purchase incluant tous les achats effectués par l'utilisateur via l'application. Chaque objet "Purchase" contient, entre autres, les attributs ID produit, purchaseToken et isAcknowledged. À l'aide de ces champs, pour chaque objet Purchase, vous pouvez déterminer s'il s'agit d'un nouvel achat à traiter ou d'un achat existant qui ne nécessite aucun traitement supplémentaire.

Pour les achats d'abonnements, le traitement revient à confirmer le nouvel achat.

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. Traitement des achats (valider et confirmer les achats)

Une fois qu'un utilisateur a effectué un achat, l'application doit le traiter en le confirmant.

De plus, la valeur de _isNewPurchaseAcknowledged est définie sur "true" lorsque la confirmation a bien été traitée.

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. Mettre fin à la connexion de facturation

Enfin, lorsqu'une activité est détruite, vous devez mettre fin à la connexion à Google Play. endConnection() est donc appelé pour cela.

BillingClientWrapper.kt

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

5. SubscriptionDataRepository

Dans BillingClientWrapper, les réponses de QueryPurchasesAsync et QueryProductDetails sont publiées respectivement dans MutableStateFlow _purchases et _productWithProductDetails, qui sont exposés en dehors de la classe avec des achats et productWithProductDetails.

Dans SubscriptionDataRepository, les achats sont traités en trois flux en fonction du produit de l'achat renvoyé: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium et hasPremiumPrepaid.

De plus, productWithProductDetails est traité en flux basicProductDetails et premiumProductDetails respectifs.

6. MainViewModel

Le plus dur est fait. Vous allez maintenant définir MainViewModel, qui n'est qu'une interface publique pour vos clients afin qu'ils n'aient pas à connaître les composants internes de BillingClientWrapper et SubscriptionDataRepository.

Tout d'abord, dans MainViewModel, nous démarrons la connexion de facturation lorsque le viewModel est initialisé.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Ensuite, les flux du dépôt sont combinés respectivement dans productsForSaleFlows (pour les produits disponibles) et userCurrentSubscriptionFlow (pour l'abonnement actuel et actif de l'utilisateur) tels qu'ils sont traités dans la classe du dépôt.

La liste des achats en cours est également mise à la disposition de l'interface utilisateur avec 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

Le userCurrentSubscriptionFlow combiné est collecté dans un bloc d'initialisation, et la valeur est publiée dans un objet MutableLiveData appelé _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 ajoute également des méthodes très utiles:

  1. Récupération des forfaits de base et des jetons d'offre

À partir de la version 5.0.0 de la bibliothèque Play Billing, tous les produits sur abonnement peuvent proposer plusieurs offres et forfaits de base, à l'exception des forfaits de base prépayés, qui ne peuvent pas comporter d'offres.

Cette méthode permet de récupérer toutes les offres et tous les forfaits de base auxquels un utilisateur est éligible grâce au nouveau concept de tags permettant de regrouper les offres associées.

Par exemple, lorsqu'un utilisateur tente de souscrire un abonnement de base mensuel, tous les forfaits de base et toutes les offres associés au produit sur abonnement de base mensuel sont tagués avec la chaîne 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. Calcul de l'offre la moins chère

Lorsqu'un utilisateur est éligible à plusieurs offres, la méthode leastPricedOfferToken() permet de calculer l'offre la moins élevée parmi celles renvoyées par retrieveEligibleOffers().

La méthode renvoie le jeton d'identifiant de l'offre sélectionnée.

Cette implémentation renvoie simplement les offres les moins chères en fonction de l'ensemble pricingPhases et ne tient pas compte des moyennes.

Une autre implémentation pourrait consister à examiner l'offre moyenne la plus basse.

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. Compilateurs BillingFlowParams

Pour lancer le parcours d'achat d'un produit particulier, le produit L'objet ProductDetails et le jeton de l'offre sélectionnée doivent être définis et utilisés pour créer un BilingFlowParams.

Pour cela, deux méthodes s'offrent à vous:

upDowngradeBillingFlowParamsBuilder() crée les paramètres des mises à niveau et des rétrogradations.

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() crée les paramètres pour les achats normaux.

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. Méthode d'achat

La méthode d'achat utilise le launchBillingFlow() de BillingClientWrapper et le BillingFlowParams pour lancer les achats.

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. Mettre fin à la connexion de facturation

Enfin, la méthode terminateBillingConnection de BillingClientWrapper est appelée sur le onCleared() d'un ViewModel.

Cela permet de mettre fin à la connexion de facturation actuelle lorsque l'activité associée est détruite.

7. Interface utilisateur

Il est maintenant temps d'utiliser tout ce que vous avez créé dans l'UI. Pour ce faire, vous utiliserez les classes composables et MainActivity.

Composables.kt

La classe des composables est entièrement fournie. Elle définit toutes les fonctions Compose utilisées pour afficher l'UI et le mécanisme de navigation entre elles.

La fonction Subscriptions affiche deux boutons: Basic Subscription et Premium Subscription.

Basic Subscription et Premium Subscription chargent chacune de nouvelles méthodes Compose qui présentent les trois forfaits de base respectifs: mensuel, annuel et prépayé.

Ensuite, il existe trois fonctions de composition de profil possibles pour un abonnement spécifique d'un utilisateur: un profil Basic et Premium renouvelable, et un profil prépayé de base ou Premium.

  • Lorsqu'un utilisateur dispose d'un abonnement Basic, le profil de base lui permet de passer à un abonnement Premium mensuel, annuel ou prépayé.
  • À l'inverse, les utilisateurs disposant d'un abonnement Premium peuvent passer à un abonnement mensuel, annuel ou de base.
  • Lorsqu'un utilisateur dispose d'un abonnement prépayé, il peut le créditer à l'aide du bouton de recharge ou convertir son abonnement prépayé en un forfait de base à renouvellement automatique correspondant.

Enfin, il existe une fonction d'écran de chargement utilisée lorsqu'une connexion à Google Play est établie et qu'un profil utilisateur est affiché.

MainActivity.kt

  1. Lorsque MainActivity est créé, le viewModel est instancié et une fonction Compose appelée MainNavHost est chargée.
  2. MainNavHost commence par une variable isBillingConnected créée à partir des données en direct billingConnectionSate du viewModel et observe les modifications, car lorsque le vieModel est instancié, il transmet billingConnectionSate à la méthode startBillingConnection de BillingClientWrapper.

isBillingConnected est défini sur "true" lorsque la connexion est établie et sur "false" dans le cas contraire.

Si la valeur est "false", la fonction Compose LoadingScreen() est chargée. Lorsqu'elle est définie sur "true", les fonctions Subscription ou profile sont chargées.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Lorsqu'une connexion de facturation est établie:

Le navController Compose est instancié

val navController = rememberNavController()

Les flux dans MainViewModel sont ensuite collectés.

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

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

Enfin, la variable LiveData destinationScreen du viewModel est observée.

La fonction Compose correspondante s'affiche en fonction de l'état d'abonnement actuel de l'utilisateur.

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. Code de solution

Le code complet de la solution est disponible dans le module correspondant.

9. Félicitations

Félicitations ! Vous avez intégré les produits sur abonnement de la bibliothèque Google Play Billing 5.0.0 dans une application très simple.

Pour obtenir une version documentée d'une application plus sophistiquée avec une configuration de serveur sécurisée, consultez l'exemple officiel.

Complément d'informations