Getting Started With In-App Purchases

Learn how to get started with in-app purchases and implement this library inside your next project. By Mattia Ferigutti.

3.5 (2) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Implementing querySkuDetailsAsync

Now you need to query the SKUs’ information. You’ll interact with the Play Console by asking for the SKUs’ details. SkuDetails are critical since they share information about the item names and price lists with the user and are required to make a purchase.

Inside the BillingHelper class, implement these lines of code:

private suspend fun querySkuDetailsAsync() {
    if (!knownInAppSKUs.isNullOrEmpty()) {
      val skuDetailsResult = billingClient.querySkuDetails(
        SkuDetailsParams.newBuilder()
          .setType(BillingClient.SkuType.INAPP)
          .setSkusList(knownInAppSKUs.toMutableList())
          .build()
      )
      // Process the result
      onSkuDetailsResponse(
        skuDetailsResult.billingResult, 
        skuDetailsResult.skuDetailsList
      )
    }
  }

Notice that the method is suspend. That’s because at its core querySkuDetails() uses coroutines to get data from the network. The method then:

  • Sets the type to BillingClient.SkuType.INAPP with setType(). You query in-app purchases only. Choose BillingClient.SkuType.SUBS if you’re using subscriptions.
  • Using setSkusList(), it passes a list of SKUs you want to get information from.
  • Then it processes the result calling onSkuDetailsResponse(). The result is a List of SkuDetails objects.

Implementing restorePurchases

Restoring the previous purchases is essential, and you should call this method every time the activity or fragment starts. Add the following snippet of code:

private suspend fun restorePurchases() {
    val purchasesResult = 
      billingClient
        .queryPurchasesAsync(BillingClient.SkuType.INAPP)
    val billingResult = purchasesResult.billingResult
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
      handlePurchase(purchasesResult.purchasesList)
    }
  }

This method calls all the SKUs available and checks if they have been purchased. It simply:

  • Calls queryPurchasesAsync() and returns a PurchasesResult object. Notice that, as before, this method is suspend as it also uses coroutines under the hood.
  • Checks whether the responseCode is valid and passes the result to handlePurchase().

Implementing onSkuDetailsResponse

Once querySkuDetailsAsync() gets the result, it passes it to onSkuDetailsRsponse to elaborate on the response. It emits the details for every SKU.

Inside the BillingHelper class, implement the following code:

private fun onSkuDetailsResponse(
  billingResult: BillingResult, 
  skuDetailsList: List<SkuDetails>?
) {
    val responseCode = billingResult.responseCode
    val debugMessage = billingResult.debugMessage
    when (responseCode) {
      BillingClient.BillingResponseCode.OK -> {
        Log.i(TAG, "onSkuDetailsResponse: $responseCode $debugMessage")
        if (skuDetailsList == null || skuDetailsList.isEmpty()) {
          Log.e(
            TAG,
            "onSkuDetailsResponse: " +
              "Found null or empty SkuDetails. " +
              "Check to see if the SKUs you requested are correctly" +
              " published in the Google Play Console."
          )
        } else {
          for (skuDetails in skuDetailsList) {
            val sku = skuDetails.sku
            val detailsMutableFlow = skuDetailsMap[sku]
            detailsMutableFlow?.tryEmit(skuDetails) ?: 
              Log.e(TAG, "Unknown sku: $sku")
          }
        }
      }
    }
  }

Here’s how the function works:

  • First, it checks if the response is positive and the skuDetailsList is neither null nor empty.
  • detailsMutableFlow gets the specific MutableStateFlow object defined inside skuDetailsMap, which is a Map that keeps a reference to all the skuDetails. Whenever you need a skuDetails you can use its SKU to get it. You first define these objects inside addSkuFlows().
  • Using tryEmit(), you emit the value and all the collectors are able to get it. tryEmit() is different from emit() since it tries to emit a value without suspending. In fact, you aren’t calling it inside a coroutine.

Implementing launchBillingFlow

Finally, you can let the user buy a product! But first, you need to show them a purchase screen and let the user pay for the item.

Inside the BillingHelper class, write this code inside your project:

fun launchBillingFlow(activity: Activity, sku: String) {
    val skuDetails = skuDetailsMap[sku]?.value
    if (null != skuDetails) {
      val flowParams = BillingFlowParams.newBuilder()
          .setSkuDetails(skuDetails)
          .build()
      billingClient.launchBillingFlow(activity, flowParams)
    }
    Log.e(TAG, "SkuDetails not found for: $sku")
  }

Here, you create the instance of the class BillingFlowParams using BillingFlowParams.Builder and pass the skuDetails inside setSkuDetails(). Then, you call launchBillingFlow() which is responsible for showing the bottom purchase screen inside your app.

You’ll see this screen call launchBillingFlow():

Screenshot of the app when you call launchBillingFlow

Implementing onPurchasesUpdated

After calling launchBillingFlow(), you need to handle the response. For example, the user might buy something within the app or initiate a purchase from Google Play Store.

Copy and paste the code inside the BillingHelper class:

override fun onPurchasesUpdated(billingResult: BillingResult, list: MutableList<Purchase>?) {
    when (billingResult.responseCode) {
      BillingClient.BillingResponseCode.OK -> if (null != list) {
        handlePurchase(list)
        return
      } else Log.d(TAG, "Null Purchase List Returned from OK response!")
      BillingClient.BillingResponseCode.USER_CANCELED -> 
        Log.i(TAG, "onPurchasesUpdated: User canceled the purchase")
      BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> 
        Log.i(TAG, "onPurchasesUpdated: The user already owns this item")
      BillingClient.BillingResponseCode.DEVELOPER_ERROR -> Log.e(
        TAG,
        "onPurchasesUpdated: Developer error means that Google Play " +
          "does not recognize the configuration. If you are just " +
          "getting started, make sure you have configured the " +
          "application correctly in the Google Play Console. " +
          "The SKU product ID must match and the APK" +
          "you are using must be signed with release keys."
      )
      else -> 
        Log.d(
          TAG,
          "BillingResult [" + billingResult.responseCode + "]: " + 
          billingResult.debugMessage
        )
    }
  }

Google Play calls onPurchasesUpdated() to deliver the result of the purchase operation launched previously with launchBillingFlow(). This method gets notifications for purchase updates.

All purchases reported here must either be consumed or acknowledged. Failure to either consume or acknowledge will result in a refund. You’ll see this concept better later.

The code is pretty straightforward:

  • You check that the response is okay with BillingClient.BillingResponseCode.OK so you have a green light to proceed with handling the purchase.
  • In case the code gives you an error, you print it in the console.

Implementing handlePurchase

handlePurchase is the most challenging method to understand. But hang tight. You’ve almost made it!

Now that you have all your purchases, it’s time to validate them. Add this code inside your BillingHelper class:

private fun handlePurchase(purchases: List<Purchase>?) {
    if (null != purchases) {
      for (purchase in purchases) {
        // Global check to make sure all purchases are signed correctly.
        // This check is best performed on your server.
        val purchaseState = purchase.purchaseState
        if (purchaseState == Purchase.PurchaseState.PURCHASED) {
          if (!isSignatureValid(purchase)) {
            Log.e(
              TAG, 
              "Invalid signature. Check to make sure your " + 
                "public key is correct."
            )
            continue
          }
          // only set the purchased state after we've validated the signature.
          setSkuStateFromPurchase(purchase)

          if (!purchase.isAcknowledged) {
            defaultScope.launch {
              for (sku in purchase.skus) {
                // Acknowledge item and change its state
                val billingResult = billingClient.acknowledgePurchase(
                    AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.purchaseToken)
                        .build()
                )
                if (billingResult.responseCode != 
                  BillingClient.BillingResponseCode.OK) {
                    Log.e(
                      TAG, 
                      "Error acknowledging purchase: ${purchase.skus}"
                    )
                } else {
                  // purchase acknowledged
                  val skuStateFlow = skuStateMap[sku]
                  skuStateFlow?.tryEmit(
                    SkuState.SKU_STATE_PURCHASED_AND_ACKNOWLEDGED
                  )
                }
              }
            }
          }
        } else {
          // purchase not purchased
          setSkuStateFromPurchase(purchase)
        }
      }
    } else {
      Log.d(TAG, "Empty purchase list.")
    }
  }

Here’s a code breakdown:

For more information, Google wrote a good article about this topic.

This function is suspend so you need to call it inside a coroutine. Pass the purchase.purchaseToken inside setPurchaseToken() to acknowledge the product.

  • A purchase won’t be acknowledged if it has a purchaseState that is not equal to PurchaseState.PURCHASED or doesn’t have a valid signature. isSignatureValid() uses the class Security to check the validity of your purchase using the public key of your Play Console. However, you should use your own server to verify the validity of a purchase.

    For more information, Google wrote a good article about this topic.

  • After validating the signature, call setSkuStateFromPurchase() to set the state of the purchase.
  • Now you check if the app acknowledged the purchase. If it wasn’t acknowledged, you need to do so. You can acknowledge a purchase using acknowledgePurchase().

    This function is suspend so you need to call it inside a coroutine. Pass the purchase.purchaseToken inside setPurchaseToken() to acknowledge the product.

  • If the app could acknowledge the purchase, it will notify all the collectors of this event.

What does acknowledging a purchase mean? It’s like saying, “I’m sure of the legitimacy of this payment. I entitle you, user, as the receiver of my product, giving you all the features you paid for”.

Once the user purchases a product, you must acknowledge it within three days, or Google Play will automatically refund and revoke the purchase.