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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With In-App Purchases
30 mins
- Getting Started
- Service Fees
- Terminology
- Content Types
- Concepts
- Setting-Up the Google Play Console
- Setting-Up the Android Project
- Loading the APK in the Play Console
- Back to the Google Play Developer Console
- Handling Purchases
- Connecting the Billing Client
- Implementing onBillingSetupFinished
- Adding Flow
- Implementing querySkuDetailsAsync
- Implementing restorePurchases
- Implementing onSkuDetailsResponse
- Implementing launchBillingFlow
- Implementing onPurchasesUpdated
- Implementing handlePurchase
- Implement setSkuStateFromPurchase
- Getting Product Details
- Implementing BillingHelper Inside The Project
- Where to Go From Here?
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
withsetType()
. You query in-app purchases only. ChooseBillingClient.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 aList
ofSkuDetails
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 aPurchasesResult
object. Notice that, as before, this method issuspend
as it also uses coroutines under the hood. - Checks whether the
responseCode
is valid and passes the result tohandlePurchase()
.
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 neithernull
nor empty. -
detailsMutableFlow
gets the specificMutableStateFlow
object defined insideskuDetailsMap
, which is a Map that keeps a reference to all theskuDetails
. Whenever you need askuDetails
you can use its SKU to get it. You first define these objects insideaddSkuFlows()
. - Using
tryEmit()
, you emit the value and all the collectors are able to get it.tryEmit()
is different fromemit()
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()
:
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 toPurchaseState.PURCHASED
or doesn’t have a valid signature.isSignatureValid()
uses the classSecurity
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 thepurchase.purchaseToken
insidesetPurchaseToken()
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.