---
title: "Achats intégrés sous Android, 2e partie : traitement des achats avec la service de facturation de Google Play"
description: "Achat intégré sous iOS : validation des reçus côté serveur. Nous expliquons pourquoi la validation des reçus est importante et comment la paramétrer."
url: "https://adapty.io/fr/blog/android-in-app-purchases-google-play-billing-library-part-2/"
language: "fr"
slug: "android-in-app-purchases-google-play-billing-library-part-2"
category: "adapty-updates"
date: "2022-09-02T19:27:00.000Z"
date_modified: "2022-09-02T19:27:00.000Z"
author: "Adapty Team"
---

# Achats intégrés sous Android, 2e partie : traitement des achats avec la service de facturation de Google Play

Dans l'article précédent, nous avons créé une classe enveloppante pour travailler avec le service de facturation:

```kotlin
import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
   interface OnQueryProductsListener {
       fun onSuccess(products: List<SkuDetails>)
       fun onFailure(error: Error)
   }
   class Error(val responseCode: Int, val debugMessage: String)
   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()
   fun queryProducts(listener: OnQueryProductsListener) {
       val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
       queryProductsForType(
           skusList,
           BillingClient.SkuType.SUBS
       ) { billingResult, skuDetailsList ->
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               val products = skuDetailsList ?: mutableListOf()
               queryProductsForType(
                   skusList,
                   BillingClient.SkuType.INAPP
               ) { billingResult, skuDetailsList ->
                   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                       products.addAll(skuDetailsList ?: listOf())
                       listener.onSuccess(products)
                   } else {
                       listener.onFailure(
                           Error(billingResult.responseCode, billingResult.debugMessage)
                       )
                   }
               }
           } else {
               listener.onFailure(
                   Error(billingResult.responseCode, billingResult.debugMessage)
               )
           }
       }
   }
   private fun queryProductsForType(
       skusList: List<String>,
       @BillingClient.SkuType type: String,
       listener: SkuDetailsResponseListener
   ) {
       onConnected {
           billingClient.querySkuDetailsAsync(
               SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
               listener
           )
       }
   }
   private fun onConnected(block: () -> Unit) {
       billingClient.startConnection(object : BillingClientStateListener {
           override fun onBillingSetupFinished(billingResult: BillingResult) {
               block()
           }
           override fun onBillingServiceDisconnected() {
               // Try to restart the connection on the next request to
               // Google Play by calling the startConnection() method.
           }
       })
   }
   override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
       // here come callbacks about new purchases
   }
}
```

Passons à l'implémentation de l'achat et améliorons notre classe.

## Conception d'un écran d'abonnement

Toute application qui propose des achats intégrés a un écran de paiement. Il existe des Règles de confidentialité et Conditions d'utilisation de Google qui définissent le strict minimum d'éléments et de textes pédagogiques qui doivent être présents dans ces écrans. Voici un résumé. Dans l'écran du paywall, vous devez être explicite sur les conditions, le coût et la durée de votre abonnement, et préciser si l'abonnement est nécessaire pour utiliser l'application. Vous devez également éviter de forcer vos utilisateurs à effectuer une action supplémentaire pour examiner les conditions.

Ici, nous utiliserons un écran simplifié de paywall comme exemple :

![android de paywall](https://adapty.io/assets/uploads/2025/06/android-de-paywall.webp)
*Échantillon pour Android de Paywall*

Nous avons les éléments suivants sur l'écran du paywall :

- Une en-tête
- Des boutons permettant de lancer le processus d'achat. Ces boutons guident l'utilisateur vers les détails généraux des options d'abonnement, tels que le titre et le coût, qui est affiché en devise locale.
- Des instructions sous forme de texte.
- Un bouton pour restaurer l'achat précédemment effectué. Cet élément est indispensable pour toute application qui propose des abonnements ou des achats non consommables.

Modification du code pour afficher les détails du produit

Il y a quatre produits dans notre exemple :

- Deux abonnements auto-renouvelables ("premium_sub_month_" et " premium_sub_year") ;
- Un produit qui ne peut pas être acheté deux fois, ou un produit non consommable ("fonction de _déverrouillage") ;
- Un produit qui peut être acheté plusieurs fois, ou un produit consommable ("coin_pack_large") ;

Pour simplifier l'exemple, nous utiliserons une activité que nous injecterons avec le BillingClientWrapper créé dans l'article précédent, ainsi qu'une mise en page avec un nombre fixe de boutons d'achat.

```kotlin
class PaywallActivity: BaseActivity() {

   @Inject
   lateinit var billingClientWrapper: BillingClientWrapper

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_paywall)

       displayProducts() //to be declared below
   }
}
```

Pour plus de commodité, nous ajouterons une carte où la clé est la référence [sku](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getSku%28)) du produit et la valeur est le bouton correspondant sur l'écran.

```kotlin
private val purchaseButtonsMap: Map<String, Button> by lazy(LazyThreadSafetyMode.NONE) {
   mapOf(
       "premium_sub_month" to monthlySubButton,
       "premium_sub_year" to yearlySubButton,
       "coin_pack_large" to coinPackLargeButton,
       "unlock_feature" to unlockFeatureButton,
   )
}
```

Déclarons une méthode pour afficher les produits dans l'interface utilisateur. Il sera basé sur la logique introduite dans notre [précédent](https://adapty.io/blog/android-in-app-subscriptions-part-1/)tutoriel.

```kotlin
private fun displayProducts() {
   billingClientWrapper.queryProducts(object : BillingClientWrapper.OnQueryProductsListener {
       override fun onSuccess(products: List<SkuDetails>>) {
           products.forEach { product ->
               purchaseButtonsMap[product.sku]?.apply {
                   text = "${product.description} for ${product.price}"
                   setOnClickListener {
                       billingClientWrapper.purchase(this@PaywallActivity, product) //will be declared below
                   }
               }
           }
       }

       override fun onFailure(error: BillingClientWrapper.Error) {
           //handle error
       }
   })
}
```

product.price est une chaîne déjà formatée qui spécifie la devise locale du compte. Elle ne nécessite pas de mise en forme supplémentaire. Toutes les autres propriétés de l'objet de la classe [SkuDetails](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails) sont également entièrement localisées.

## Démarrer le processus d'achat

Pour traiter l'achat, nous devons invoquer la méthode [launchBillingFlow()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchBillingFlow%28android.app.Activity,%20com.android.billingclient.api.BillingFlowParams)) depuis le fil d'exécution (thread) principal de l'application.

Pour ce faire, nous allons ajouter un mode d'achat() au BillingClientWrapper.

```kotlin
fun purchase(activity: Activity, product: SkuDetails) {
   onConnected {
       activity.runOnUiThread {
           billingClient.launchBillingFlow(
               activity,
               BillingFlowParams.newBuilder().setSkuDetails(product).build()
           )
       }
   }
}
```

La méthode launchBillingFlow() n'a pas de fonction de rappel. La réponse sera renvoyée à la méthode onPurchasesUpdated(). Vous vous souvenez que nous l'avons déclaré dans [l'article précédent](https://adapty.io/blog/android-in-app-subscriptions-part-1/)  et que nous l'avons gardé pour plus tard ? Eh bien, nous en aurons besoin maintenant.

La méthode onPurchasesUpdated() est appelée chaque fois qu'un résultat est obtenu à la suite de l'interaction de l'utilisateur avec la boîte de dialogue d'achat. Il peut s'agir d'un achat réussi, ou d'une annulation d'achat causée par la fermeture de la boîte de dialogue par l'utilisateur, auquel cas nous obtiendrons le code [BillingResponseCode.*USER_CANCELED*](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#USER_CANCELED). Il peut également s'agir de l'un des autres messages d'erreur possibles.

De manière similaire à l'interface OnQueryProductsListener de l'article précédent, nous allons déclarer une interface OnPurchaseListener dans la classe BillingClientWrapper. Via cette interface, nous recevrons soit l'achat (un objet de la classe Purchase), soit un message d'erreur que nous avons déclaré dans [le guide précédent](https://adapty.io/blog/android-in-app-subscriptions/). Dans le prochain article, nous aborderons le cas où l'objet de la classe Achat peut être nul même si l'achat a été effectué avec succès.

```kotlin
interface OnPurchaseListener {
   fun onPurchaseSuccess(purchase: Purchase?)
   fun onPurchaseFailure(error: Error)
}

var onPurchaseListener: OnPurchaseListener? = null
```

Ensuite, nous allons l'implémenter dans  PaywallActivity :

```kotlin
class PaywallActivity: BaseActivity(), BillingClientWrapper.OnPurchaseListener {

   @Inject
   lateinit var billingClientWrapper: BillingClientWrapper

   private val purchaseButtonsMap: Map<String, Button> by lazy(LazyThreadSafetyMode.NONE) {
       mapOf(
           "premium_sub_month" to monthlySubButton,
           "premium_sub_year" to yearlySubButton,
           "coin_pack_large" to coinPackLargeButton,
           "unlock_feature" to unlockFeatureButton,
       )
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_paywall)

       billingClientWrapper.onPurchaseListener = this

       displayProducts()
   }

   override fun onPurchaseSuccess(purchase: Purchase?) {
       //handle successful purchase
   }

   override fun onPurchaseFailure(error: BillingClientWrapper.Error) {
       //handle error or user cancelation
   }

   private fun displayProducts() {
       billingClientWrapper.queryProducts(object : BillingClientWrapper.OnQueryProductsListener {
           override fun onSuccess(products: List<SkuDetails>) {
               products.forEach { product ->
                   purchaseButtonsMap[product.sku]?.apply {
                       text = "${product.description} for ${product.price}"
                       setOnClickListener {
                           billingClientWrapper.purchase(this@PaywallActivity, product)
                       }
                   }
               }
           }

           override fun onFailure(error: BillingClientWrapper.Error) {
               //handle error
           }
       })
   }
}
```

Ajoutons un peu de logique à onPurchaseUpdated() :

```kotlin
override fun onPurchasesUpdated(
   billingResult: BillingResult,
   purchaseList: MutableList<Purchase>?
) {
   when (billingResult.responseCode) {
       BillingClient.BillingResponseCode.OK -> {
           if (purchaseList == null) {
               //to be discussed in the next article
               onPurchaseListener?.onPurchaseSuccess(null)
               return
           }

           purchaseList.forEach(::processPurchase) //to be declared below
       }
       else -> {
           //error occured or user canceled
           onPurchaseListener?.onPurchaseFailure(
               BillingClientWrapper.Error(
                   billingResult.responseCode,
                   billingResult.debugMessage
               )
           )
       }
   }
}
```

Si la liste d'achats n'est pas vide, nous devrons d'abord sélectionner les articles dont l'état d'achat est égal à [PurchaseState.PURCHASED](https://developer.android.com/google/play/billing/integrate#pending), car il y a aussi des achats en [en attente](https://developer.android.com/google/play/billing/integrate#pending). Si c'est le cas, le flux d'utilisateurs se termine ici. Selon [les documents](https://developer.android.com/google/play/billing/integrate#process), nous devons ensuite vérifier l'achat sur notre serveur. Nous en parlerons dans les articles qui suivront dans cette série. Une fois la vérification du serveur terminée, vous devez livrer le contenu et le faire savoir à Google. Sans ce dernier, l'achat sera automatiquement remboursé dans les trois jours. Il est intéressant de noter que cette politique est propre à Google Play - iOS n'en impose aucune. Vous avez deux façons d'accuser réception de votre contenu à l'utilisateur :

- Via [acknowledgePurchase()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#acknowledgepurchase) du côté client ;
- Via [Product.Purchases.Acknowledge](https://developers.google.com/android-publisher/api-ref/purchases/products/acknowledge)/[Purchases.Subscriptions.Acknowledge](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/acknowledge) du côté back-end.

S'il s'agit d'un produit consommable, nous devons invoquer la méthode [consumeAsync()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#consumeasync) à la place. Il reconnaît l'achat sous le capot, tout en rendant possible un nouvel achat du produit. Cela ne peut se faire qu'avec le Service de Facturation. Pour une raison quelconque, le Google Play Developer API n'offre aucun moyen de le faire du côté du back-end. Il est assez curieux que, contrairement à Google Play, l'App Store et l'AppGallery configurent le produit comme consommable via App Store Connect et AppGallery Connect, respectivement. Cependant, ces produits de la galerie d'applications doivent être consommés de manière explicite.

Écrivons les méthodes de reconnaissance et de consommation, ainsi que deux versions de la méthode processPurchase() pour tenir compte du fait que nous exécutons ou non notre propre back-end.

```kotlin
private fun acknowledgePurchase(
   purchase: Purchase,
   callback: AcknowledgePurchaseResponseListener
) {
   onConnected {
       billingClient.acknowledgePurchase(
           AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
               .build(),
           callback::onAcknowledgePurchaseResponse
       )
   }
}

private fun consumePurchase(purchase: Purchase, callback: ConsumeResponseListener) {
   onConnected {
       billingClient.consumeAsync(
           ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
       ) { billingResult, purchaseToken ->
           callback.onConsumeResponse(billingResult, purchaseToken)
       }
   }
}
```

Sans vérification du serveur :

```kotlin
private fun processPurchase(purchase: Purchase) {
   if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
       onPurchaseListener?.onPurchaseSuccess(purchase)

       if (purchase.skus.firstOrNull() == "coin_pack_large") {
           //consuming our only consumable product
           consumePurchase(purchase) { billingResult, purchaseToken ->
               if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                   //implement retry logic or try to consume again in onResume()
               }
           }
       } else if (!purchase.isAcknowledged) {
           acknowledgePurchase(purchase) { billingResult ->
               if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                   //implement retry logic or try to acknowledge again in onResume()
               }
           }
       }
   }
}
```

Avec vérification du serveur :

```kotlin
private fun processPurchase(purchase: Purchase) {
   if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
       api.verifyPurchase(purchase.purchaseToken) { error ->
           if (error != null) {
               onPurchaseListener?.onPurchaseSuccess(purchase)

               if (purchase.skus.firstOrNull() == "coin_pack_large") {
                   //consuming our only consumable product
                   billingClient.consumeAsync(
                       ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
                           .build()
                   ) { billingResult, purchaseToken ->
                       if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                           //implement retry logic or try to consume again in onResume()
                       }
                   }
               }
           } else {
               //handle verification error
           }
       }
   }
}
```

Dans les articles suivants, nous aborderons plus en détail la vérification des serveurs pour les achats.

Bien entendu, nous pourrions également mettre en œuvre l'accusé de réception du deuxième exemple du côté client. Cependant, si vous pouvez faire quelque chose en arrière-plan, alors vous devriez le faire. Si l'accusé de réception ou le processus de consommation génère des erreurs, celles-ci ne peuvent être ignorées. Si aucune de ces mesures n'est exécutée dans les 3 jours suivant l'obtention du statut [PurchaseState.*PURCHASED*](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#PURCHASED), l'achat sera annulé et remboursé. Ainsi, s'il est impossible de le faire en arrière-plan et que vous continuez à obtenir l'erreur après un certain nombre de tentatives, il existe une solution de rechange fiable. Vous devrez obtenir les achats actuels de l'utilisateur via une méthode de cycle de vie, telle que [onStart() ou onResume()](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.Event), et continuer à essayer, en espérant que l'utilisateur ouvrira l'application dans les 3 jours tout en étant connecté à Internet. :)

Par conséquent, la version actuelle de la classe BillingClientWrapper ressemblera à ceci :

```kotlin
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.*

class BillingClientWrapper(context: Context, private val api: Api) : PurchasesUpdatedListener {

   interface OnQueryProductsListener {
       fun onSuccess(products: List<SkuDetails>)
       fun onFailure(error: Error)
   }

   interface OnPurchaseListener {
       fun onPurchaseSuccess(purchase: Purchase?)
       fun onPurchaseFailure(error: Error)
   }

   var onPurchaseListener: OnPurchaseListener? = null

   class Error(val responseCode: Int, val debugMessage: String)

   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()

   fun purchase(activity: Activity, product: SkuDetails) {
       onConnected {
           activity.runOnUiThread {
               billingClient.launchBillingFlow(
                   activity,
                   BillingFlowParams.newBuilder().setSkuDetails(product).build()
               )
           }
       }
   }

   fun queryProducts(listener: OnQueryProductsListener) {
       val skusList = listOf("premium_sub_month", "premium_sub_year", "coin_pack_large", "unlock_feature")

       queryProductsForType(
           skusList,
           BillingClient.SkuType.SUBS
       ) { billingResult, skuDetailsList ->
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               val products = skuDetailsList ?: mutableListOf()
               queryProductsForType(
                   skusList,
                   BillingClient.SkuType.INAPP
               ) { billingResult, skuDetailsList ->
                   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                       products.addAll(skuDetailsList ?: listOf())
                       listener.onSuccess(products)
                   } else {
                       listener.onFailure(
                           Error(billingResult.responseCode, billingResult.debugMessage)
                       )
                   }
               }
           } else {
               listener.onFailure(
                   Error(billingResult.responseCode, billingResult.debugMessage)
               )
           }
       }
   }

   private fun queryProductsForType(
       skusList: List<String>,
       @BillingClient.SkuType type: String,
       listener: SkuDetailsResponseListener
   ) {
       onConnected {
           billingClient.querySkuDetailsAsync(
               SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
               listener
           )
       }
   }

   private fun onConnected(block: () -> Unit) {
       billingClient.startConnection(object : BillingClientStateListener {
           override fun onBillingSetupFinished(billingResult: BillingResult) {
               block()
           }

           override fun onBillingServiceDisconnected() {
               // Try to restart the connection on the next request to
               // Google Play by calling the startConnection() method.
           }
       })
   }

   override fun onPurchasesUpdated(
       billingResult: BillingResult,
       purchaseList: MutableList<Purchase>?
   ) {
       when (billingResult.responseCode) {
           BillingClient.BillingResponseCode.OK -> {
               if (purchaseList == null) {
                   //to be discussed in the next article
                   onPurchaseListener?.onPurchaseSuccess(null)
                   return
               }

               purchaseList.forEach(::processPurchase)
           }
           else -> {
               //error occured or user canceled
               onPurchaseListener?.onPurchaseFailure(
                   BillingClientWrapper.Error(
                       billingResult.responseCode,
                       billingResult.debugMessage
                   )
               )
           }
       }
   }

   private fun processPurchase(purchase: Purchase) {
       if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
           api.verifyPurchase(purchase.purchaseToken) { error ->
               if (error != null) {
                   onPurchaseListener?.onPurchaseSuccess(purchase)

                   if (purchase.skus.firstOrNull() == "coin_pack_large") {
                       //consuming our only consumable product
                       billingClient.consumeAsync(
                           ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
                               .build()
                       ) { billingResult, purchaseToken ->
                           if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                               //implement retry logic or try to consume again in onResume()
                           }
                       }
                   }
               } else {
                   //handle verification error
               }
           }
       }
   }

   private fun acknowledgePurchase(
       purchase: Purchase,
       callback: AcknowledgePurchaseResponseListener
   ) {
       onConnected {
           billingClient.acknowledgePurchase(
               AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
                   .build(),
               callback::onAcknowledgePurchaseResponse
           )
       }
   }

   private fun consumePurchase(purchase: Purchase, callback: ConsumeResponseListener) {
       onConnected {
           billingClient.consumeAsync(
               ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
           ) { billingResult, purchaseToken ->
               callback.onConsumeResponse(billingResult, purchaseToken)
           }
       }
   }
}
```

Vous pouvez vous demander pourquoi les boutons sont actifs pour tous les produits, que l'utilisateur les ait déjà achetés ou non. Ou bien, vous avez peut-être des questions sur ce qui se passera si vous achetez les deux abonnements. Le premier abonnement remplacera-t-il le second, ou coexisteront-ils ? Consulter les articles à venir pour toutes les réponses :)
