Setup Google Play In-App Purchase With Play Billing Library (Kotlin)

Edit module/app build.gradle to add Play Billing Library dependencies.

dependencies {
    // https://mvnrepository.com/artifact/com.codeslap/android-billing-library
    implementation 'com.android.billingclient:billing:1.0'
}

Connect to Google Play

Create BillingClient connect to Google Play.

Handle your purchases at // TODO: handlePurchase(purchase).

fun setupBilling(context: Context) {    billingClient = BillingClient.newBuilder(context).setListener { responseCode, purchases ->        // also called when new purchases are made        if (responseCode === BillingClient.BillingResponse.OK && purchases != null) {            for (purchase in purchases) {                if (verifyValidSignature(purchase.originalJson, purchase.signature)) {                    // TODO: handlePurchase(purchase)                }            }        } else if (responseCode === BillingClient.BillingResponse.USER_CANCELED) {            // Handle an error caused by a user cancelling the purchase flow.        } else {            // Handle any other error codes.        }    }.build()}

NOTE: the verifyValidSignature method will be explained later at Verify Purchase Signature section.

I created a startConnection method which accept lamda function to execute if connection is successful.

fun startConnection(run: () -> Unit) {    billingClient.startConnection(object : BillingClientStateListener {        override fun onBillingSetupFinished(responseCode: Int) {            if (responseCode == BillingClient.BillingResponse.OK) {                // isBillingConnected = true                // The billing client is ready. You can query purchases here.                run()            }        }        override fun onBillingServiceDisconnected() {            // Try to restart the connection on the next request to            // Google Play by calling the startConnection() method.            // isBillingConnected = false        }    })}

Usage.

setupBilling(context)startConnection {    querySkuDetails()    queryPurchases()}

Query Product/SKU Details

If you are using both In-app purchase and Subscription, you would need to call BillingClient.querySkuDetailsAsync twice with different SkuType.

Handle your products at // TODO: handleProduct(skuDetails).

NOTE: For querySkuDetailsAsync to work, you need to publish your app to Google Play (you can publish as alpha or beta for testing purpose at Google Play Console - it won't appear to the public in Google Play as long as you didn't create a production release). Then, you need to create the SKUs.

private fun querySkuDetails() {    // for in-app purchase    billingClient.let { billingClient ->        val skuList = arrayListOf("pro", "backer")        val params = SkuDetailsParams.newBuilder()        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)        billingClient.querySkuDetailsAsync(params.build()) { responseCode, skuDetailsList ->            if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) {                for (skuDetails in skuDetailsList) {                    // val sku = skuDetails.sku                    // val price = skuDetails.price                    // TODO: handleProduct(skuDetails)                }            }        }    }    // for subscription    billingClient.let { billingClient ->        val skuList = arrayListOf("patron")        val params = SkuDetailsParams.newBuilder()        params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)        billingClient.querySkuDetailsAsync(params.build()) { responseCode, skuDetailsList ->            if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) {                for (skuDetails in skuDetailsList) {                    // TODO: handleProduct(skuDetails)                }            }        }    }}

Query Purchases

If you are using both In-app purchase and Subscription, you would need to call BillingClient.queryPurchases twice with different SkuType.

Handle your purchases at // TODO: handlePurchase(purchase).

NOTE: There is 2 places where you need to handlePurchase, which is BillingClient.Builder.setListener (as shown in setupBilling) and queryPurchases.

private fun queryPurchases() {    val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)    if (billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS) == BillingClient.BillingResponse.OK) {        val subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS)        if (subscriptionResult.responseCode == BillingClient.BillingResponse.OK) {            purchasesResult.purchasesList.addAll(                    subscriptionResult.purchasesList)        }    }    for (purchase in purchasesResult.purchasesList) {        if (BillingHelper.verifyValidSignature(purchase.originalJson, purchase.signature)) {            // TODO: handlePurchase(purchase)        }    }}

Purchase Product

BillingClient.launchBillingFlow will trigger onPurchasesUpdated (which we handle at setupBilling)

fun launchBuy(context: Activity, skuDetails: SkuDetails) {    val flowParams = BillingFlowParams.newBuilder()            .setSku(skuDetails.sku)            .setType(skuDetails.type)            .build()    val responseCode = billingClient.launchBillingFlow(context, flowParams)}

To handle cases where the purchase is successful but the activity had gone into background/sleep, perform queryPurchases at onResume just to be sure.

class UpgradeActivity : AppCompatActivity() {    override fun onResume() {        super.onResume()        if (billingClient != null) { //  && billingClient.isReady            // queryPurchases()            executeRequest(::queryPurchases)        }    }}

To safely call queryPurchases, I created executeRequest function to check if billingClient is ready (as it might get disconnected).

fun executeRequest(run: () -> Unit) {    if (billingClient.isReady) {        run()    }    else {        startConnection(run)    }}executeRequest(::queryPurchases)

Special Notes

There is a section on Consuming a purchase which this tutorial didn't cover.

It may take some time for Google Play to update the local cache. If your user makes a purchase from one device, any other devices they are using are not updated immediately.

I believe it is more secure not to keep purchase status in local preference or database, but to always perform queryPurchases before activating/unlock the purchase item or feature. This is to avoid if someone figured out a way to tamper with the local storage and also the get updated status on subscription or disputed purchases.

Verify Purchase Signature

Based on Play Billing - Purchase documentation

Security Recommendation: When you receive the purchase response from Google Play, perform a secure validation on your own backend. Don't trust the client, since an Android app could be decompiled and your security checks replaced with stubs. Instead, call the get() method on the Purchases.subscriptions resource (for subscriptions) or the Purchases.products resource (for products) from the Subscriptions and In-App Purchases API, or check the returned data signature.

What does it means? I assume what it means the user could fake the purchase by changing the android client code.

How to counter the fraud/hack?

  • There is a server side API which allow you the verify the purchase on your own server. I assume this is only useful if you keep track of purchase on the server and most of your user data is on the server, like a Game.
  • I am building an offline app (no server, all data stored on device only), so I believe the advice doesn't quite apply for my case.
  • There is a local signature check example in TrivialDrive_v2, with the note Security-related methods. For a secure implementation, all of this code should be implemented on a server that communicates with the application on the device.. If the user could decompile and change my code, I assume such local signature check would be quite useless as well (unless they hack the google play API instead of changing my code).

Conclusion:

  • If you have a server and user data is stored on server, you should definitely use the server data as single source of truth and verify all purchases using the server api on your server.
  • If you have an offline app, then you could ignore signature verification or implement local signature check example.

I would recommend to create a stub verifyValidSignature method, which you can implement the appropriate check later.

fun verifyValidSignature(signedData: String, signature: String): Boolean {    // perform server verification or implement local signature check    return true}

NOTE: My assumption could be wrong. Let me know if you have a better understanding of this.

References:

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.