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 noteSecurity-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: