This is the second article in our series about implementing in-app purchases on Android. You probably might be interested in reading all of our tutorials on this topic:
In the previous article, we created a wrapper class to work with the Billing Library:
Let's proceed to the purchase implementation and improve our class.
Any app that features in-app purchases has a paywall screen. There are Google policies that define the bare minimum of elements and instructional texts that must be present in such screens. Here’s a summary. In the paywall screen, you must be explicit about your subscription conditions, cost, and duration, as well as specify whether the subscription is necessary to use the app. You must also avoid forcing your users to perform any additional action to review the conditions.
Here, we’ll use a simplified paywall screen as an example:
We have the following elements on the paywall screen:
Tweaking the code to display product details
There are four products in our example:
To simplify the example, we’ll use an Activity which we’ll inject with the BillingClientWrapper created in the previous article, as well as a layout with a fixed number of purchase buttons.
For convenience, we’ll add a map where the key is the product’s sku, and the value is the corresponding button on the screen.
Let’s declare a method to display the products in the UI. It will be based on the logic introduced in our previous tutorial.
product.price is an already formatted string that specifies the local currency for the account. It doesn’t need any extra formatting. All the other properties of the SkuDetails class object come fully localized as well.
To process the purchase, we have to invoke the launchBillingFlow() method from the app’s main thread.
We’ll add a purchase() method to the BillingClientWrapper to do that.
The launchBillingFlow() method has no callback. The response will return to the onPurchasesUpdated() method. Do you remember us declaring it in the previous article and then saving it for later on? Well, we’ll need it now.
The onPurchasesUpdated() method is called whenever there’s any outcome out of the user’s interaction with the purchase dialog. This can be a successful purchase, or a purchase cancellation caused by the user closing the dialog, in which case we’ll get the BillingResponseCode.USER_CANCELED code. Alternatively, it can be any of the other possible error messages.
In a similar fashion to the OnQueryProductsListener interface from the previous article, we’ll declare an OnPurchaseListener interface in the BillingClientWrapper class. Via that interface, we’ll receive either the purchase (a Purchase class object) or an error message declared by us in the previous guide. In the next one, we’ll discuss the case where the Purchase class object can be null even if the purchase was successful.
Next, we’ll implement it in the PaywallActivity:
Let’s add some logic to onPurchaseUpdated():
If purchaseList isn’t empty, we’ll first have to pick out the items whose purchaseState equals PurchaseState.PURCHASED, since there are pending purchases also. If that’s the case, the user flow ends here. According to the docs, we should then verify the purchase on our server. We’ll cover this in the articles to follow in this series. Once the server verification is completed, you must deliver the content and let Google know about it. Without the latter, the purchase will get automatically refunded in three days. It’s quite interesting that this policy is unique to Google Play — iOS doesn’t impose any similar ones. You have two ways of how to acknowledge delivering your content to the user:
If dealing with a consumable product, we have to invoke the consumeAsync() method instead. It acknowledges the purchase under the hood, while also making it possible to buy the product again. This can only be done with the Billing Library. For some reason, the Google Play Developer API doesn’t offer any way to do this on the backend side. It’s quite curious that unlike Google Play, both App Store and AppGallery configure the product as consumable via App Store Connect and AppGallery Connect, respectively. Though, such AppGallery products should be consumed in an explicit manner as well.
Before next step, let me pause to remind you that Adapty SDK makes implementing in-app purchases into an Android App much easier. So, I would highly recommend you to have a look at it and schedule a demo.
Let’s write the methods for acknowledge and consume, as well as two versions of the processPurchase() method to account for whether or not we’re running our own backend.
Without server verification:
With server verification:
In the articles to follow, we’ll cover server verification for purchases in more detail.
Of course, we could also implement the acknowledgement in the second example on the client side. However, if you can do something on the backend, then you should. If either the acknowledgement or the consumption process throws any errors, these cannot be ignored. In case none of these get executed within 3 days after the purchase receiving the PurchaseState.PURCHASED status, it will be cancelled and refunded. So if it’s impossible to do on the backend and keeps throwing an error after a number of attempts, there’s a reliable workaround. You’ll have to get the user’s current purchases via some lifecycle method, such as onStart() или onResume(), and keep trying in hopes that the user will open the app within 3 days while connected to the internet. :)
Therefore, the current version of the BillingClientWrapper class will look like this:
You may want to ask why the buttons are active for all products, no matter whether the user has already purchased these or not. Or, you may have some questions about what happens if you buy both subscriptions. Will the first subscription replace the second one, or will they co-exist? See the upcoming articles for all the answers :)