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

1. Présentation

Le système de facturation de Google Play vous permet de vendre des produits et des contenus numériques dans votre application Android. Il s'agit de la méthode la plus directe pour vendre des produits intégrés à votre application afin de monétiser votre application. Cet atelier de programmation vous explique comment utiliser la bibliothèque Google Play Billing pour vendre des abonnements tout en intégrant les détails de votre application dans le détail.

Elle décrit également les concepts liés aux abonnements, comme les forfaits de base, les offres, les balises et les forfaits prépayés. Pour en savoir plus sur les abonnements sur 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 basée sur un abonnement simple. Cette application étant déjà créée pour vous, il vous suffit d'ajouter la partie facturation. Comme illustré dans la figure 1, l'utilisateur s'inscrit à l'un des forfaits et/ou offres de base proposés via deux produits d'abonnement renouvelables (payants et de base) ou à un prépaiement non renouvelable. C'est tout. Les forfaits de base sont respectivement mensuels et annuels. L'utilisateur peut passer à un abonnement prépayé plus élevé qu'un abonnement prépayé.

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 a pour but d'encapsuler les interactions avec le client de la bibliothèque Play Billing, mais n'est pas obligatoire dans votre propre intégration.
  • SubscriptionDataRepository : dépôt de facturation de votre application contenant une liste de l'inventaire des produits sur abonnement de l'application (c'est-à-dire les articles à vendre), ainsi qu'une liste des variables ShareFlow qui permettent de recueillir l'état des achats et des informations détaillées sur le produit
  • 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 terminée, l'architecture de votre application doit ressembler à l'illustration 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 offres et les forfaits de base disponibles dans l'application
  • Lancer le parcours de facturation avec les paramètres appropriés
  • Proposer des produits sur abonnement prépayés

Cet atelier de programmation porte sur Google Play Billing. Les concepts et les blocs de code non pertinents ne sont pas abordés, mais vous sont fournis afin que vous puissiez simplement les copier et les coller.

Prérequis

  • Version récente d'Android Studio (>= Arctic Fox | 2020.3.1)
  • Appareil équipé d'Android 8.0 ou version ultérieure
  • L'exemple de code fourni pour vous sur GitHub (instructions dans les sections suivantes)
  • Connaissances moyennes du développement Android sur Android Studio
  • Savoir comment publier une application sur le Google Play Store
  • Expérience modérée de la rédaction de code Kotlin

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 devrez obtenir le code et l'ouvrir dans l'environnement de développement de votre choix. 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-codelabs

3. Les fondations

Quel est notre point de départ ?

Vous allez utiliser une application de profil utilisateur de base conçue pour cet atelier de programmation. Ce code a été simplifié pour afficher les concepts que nous voulons illustrer et il n'a pas été conçu pour être utilisé en production. Si vous décidez de réutiliser tout ce code dans une application en production, veillez à suivre les bonnes pratiques et à tester complètement votre code.

Importer le projet dans Android Studio

L'atelier de programmation sur la facturation est une application de base qui ne contient pas une implémentation de facturation Google Play. Lancez Android Studio et importez l'atelier de programmation sur la facturation en sélectionnant **Open > billing-codelab/build.gradle**

Le projet comporte deux packages:

  • L'atelier de programmation contient l'application de squelette, mais ne dispose pas des dépendances requises ni de toutes les méthodes à implémenter.
  • solution est déjà terminée. Elle peut faire office de guide lorsque vous êtes bloqué.

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

  • BillingClientWrapper est un wrapper qui isole les méthodes [BillingClient] Google Play Billing nécessaires pour avoir une implémentation simple et envoie des réponses au référentiel de données pour le traitement.
  • SubscriptionDataRepository permet d'extraire la source de données Google Play Billing (c'est-à-dire la bibliothèque cliente de facturation) et convertit 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'interface utilisateur dans une même classe.
  • MainState est une classe de données permettant de gérer l'état.
  • MainViewModel permet de conserver les données et les états liés à la facturation utilisés dans l'interface utilisateur. Il combine tous les flux dans SubscriptionDataRepository dans un seul objet d'état.
  • MainViewModelFactory est une implémentation de ViewModelProviders.Factory qui permet d'instancier MainViewModel.
  • L'objet Constants comporte les constantes utilisées par plusieurs classes.
  • MainActivity est la classe d'activité principale qui charge les objets composables pour l'interface utilisateur.

Gradle

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

dependencies {
    val billing_version = "5.0.0"

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

Console

Dans le cadre de cet atelier de programmation, vous devez créer les deux offres de produits sur abonnement suivantes dans la Google Play Console:

  • 1 abonnement de base avec l'ID produit up_basic_sub

Le produit doit comporter trois forfaits de base (deux à renouvellement automatique et un prépayé) avec des 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éritent des balises 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 des 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.

Vous pouvez ajouter des offres aux forfaits de base. Les offres héritent des balises des forfaits de base associés.

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

4. La configuration du client de facturation

Pour cette section, vous allez travailler dans la classe BillingClientWrapper.

À la fin de l'atelier, tous les éléments nécessaires à l'instanciation du client de facturation seront disponibles, ainsi que toutes les méthodes associées.

  1. Initialiser un ClientClient

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

InvoiceClientWrapper.kt

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

Après avoir créé un BillingClient, nous devons établir une connexion à 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 la configuration du client terminée et le processus prêt à envoyer d'autres requêtes.

InvoiceClientWrapper.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 les achats existants sur Google Play Billing

Une fois la connexion établie avec Google Play, vous pouvez interroger les achats que l'utilisateur a déjà effectués en appelant queryPurchasesAsync().

InvoiceClientWrapper.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 désormais rechercher des produits disponibles et les présenter aux utilisateurs. Pour interroger Google Play et obtenir des informations détaillées sur le produit sur abonnement, nous allons appeler queryProductDetailsAsync(). L'interrogation des détails d'un produit est une étape importante avant de le présenter aux utilisateurs, car il renvoie des informations localisées.

InvoiceClientWrapper.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 une carte pour _productWithProductDetails.

Notez également que la requête doit renvoyer ProductDetails. Si cela ne se produit pas, le problème est probablement lié au fait que les produits configurés dans la Play Console n'ont pas été activés ou que vous n'avez pas publié de build qui utilise la dépendance du client de facturation sur les canaux de publication.

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

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

InvoiceClientWrapper.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 de Google Play (soit en appuyant sur le bouton"Acheter"pour finaliser l'achat, soit en appuyant sur le bouton"Retour") pour annuler l'achat, le rappel onPurchaseUpdated() renvoie le résultat d'achat à votre application. En fonction de BillingResult.responseCode, vous pouvez ensuite déterminer si l'utilisateur a réussi à acheter le produit. Si la valeur est responseCode == OK, cela signifie que l'achat a bien été effectué.

onPurchaseUpdated() renvoie une liste d'objets Purchase qui inclut tous les achats que l'utilisateur a effectués via l'application. Entre autres champs, chaque objet Purchase contient l'ID de produit, le jeton d'achat et les attributs isAcknowledged. Grâce à ces champs, vous pouvez déterminer, pour chaque objet Purchase, s'il s'agit d'un nouvel achat devant être traité ou d'un achat existant qui ne requiert pas de traitement supplémentaire.

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

InvoiceClientWrapper.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 traiter cet achat en l'accusé de réception.

De plus, la valeur de _isNewPurchaseAcknowledged est définie sur"true"lorsque l'accusé de réception est traité.

InvoiceClientWrapper.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. Désactiver la connexion à la facturation

Enfin, lorsqu'une activité est détruite, vous voulez interrompre la connexion à Google Play pour que endConnection() soit invité à le faire.

InvoiceClientWrapper.kt

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

5. Abonnement aux données d'abonnement

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

Dans SubscriptionDataRepository, les achats sont traités en trois flux en fonction du produit acheté: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium et hasPremiumPrepaid.

En outre, productWithProductDetails est traité dans les flux basicProductDetails et premiumProductDetails respectifs.

6. MainViewModel

Le plus difficile est fait. Vous allez maintenant définir MainViewModel, c'est-à-dire une interface publique qui permet à vos clients de ne pas connaître les éléments internes de BillingClientWrapper et SubscriptionDataRepository.

Tout d'abord dans MainViewModel, nous démarrons la connexion de facturation lors de l'initialisation de viewModel.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Les Flows du dépôt sont ensuite combinés dans productsForSaleFlows (pour les produits disponibles) et userCurrentSubscriptionFlow (pour l'abonnement actuel et actif de l'utilisateur) dans la classe de dépôt.

La liste des achats en cours est également disponible dans 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 init, 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 propose également d'autres méthodes très utiles:

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

À partir de la version 5.0.0 de la bibliothèque Play Billing, tous les produits sur abonnement peuvent présenter 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 les forfaits de base auxquels un utilisateur peut prétendre en utilisant le nouveau concept de balises utilisé pour regrouper les offres associées.

Par exemple, lorsqu'un utilisateur tente de souscrire un abonnement de base mensuel, tous les forfaits et offres de base associés au produit d'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 peut bénéficier de plusieurs offres, la méthode leastPricedOfferToken() est utilisée pour calculer l'offre la plus faible parmi celles renvoyées par retrieveEligibleOffers().

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

Cette configuration renvoie simplement les offres les moins chères en termes de pricingPhases et ne tient pas compte des moyennes.

Vous pouvez également envisager d'utiliser plutôt l'offre dont le prix moyen est le plus bas.

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. Créateurs de FlowFlowParams

Pour lancer le parcours d'achat d'un produit particulier, vous devez définir le jeton ProductDetails et le jeton de l'offre sélectionné afin de créer des paramètres BilingFlowParam.

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

upDowngradeBillingFlowParamsBuilder() crée les paramètres pour le passage à une édition supérieure ou inférieure.

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 standards.

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 BillingClientWrapper's launchBillingFlow() et 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 à l'association de facturation

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

Cette action entraîne l'arrêt de la connexion de facturation actuelle lorsque l'activité associée est détruite.

7. L'interface utilisateur

Vous pouvez maintenant utiliser tout ce que vous avez créé dans l'interface utilisateur. Pour vous aider, vous allez travailler avec les classes "Composables" et "MainActivity".

Composables.kt

La classe Composables est entièrement fournie et définit toutes les fonctions de rédaction utilisées pour afficher l'interface utilisateur 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 affichent les trois forfaits de base respectifs: mensuel, annuel et prépayé.

Ensuite, trois profils de rédaction sont disponibles pour un abonnement spécifique: un profil de base renouvelable, un abonnement Premium renouvelable et un profil prépayé de base ou prépayé.

  • Lorsqu'un utilisateur possède un abonnement Basic, le profil de base lui permet de passer à un abonnement premium mensuel, annuel ou prépayé.
  • À l'inverse, lorsqu'un utilisateur dispose d'un abonnement premium, il peut passer à un abonnement mensuel, annuel ou prépayé.
  • Lorsqu'un utilisateur possède un abonnement prépayé, il peut créditer son abonnement avec le bouton de recharge ou le convertir en forfait de base à renouvellement automatique.

Enfin, une fonction de chargement d'écran est 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 de rédaction appelée MainNavHost est chargée.
  2. MainNavHost commence par une variable isBillingConnected créée à partir de la valeur LiveData de la billingConnectionSateModelModel et observe les modifications, car lorsque celle-ci est instanciée, elle transmet billingConnectionSate à la méthode BillingClientWrapperBilling#Connection.

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

Lorsque la valeur est"false", la fonction de rédaction LoadingScreen() est chargée. Lorsque la valeur est"true", les fonctions Subscription ou de profil sont chargées.

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

Le navController de rédaction d'e-mail est instancié

val navController = rememberNavController()

Ensuite, les flux dans MainViewModel sont collectés.

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

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

Enfin, la variable LiveData destinationScreen de viewModel's est observée.

L'état de l'abonnement actuel de l'utilisateur s'affiche en fonction de la fonction de rédaction correspondante.

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 la solution

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

9. Félicitations

Félicitations, vous avez bien intégré les produits 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