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:
- Android in-app purchases, part 1: configuration and adding to the project.
- Android in-app purchases, part 2: processing purchases with the Google Play Billing Library.
- Android in-app purchases, part 3: retrieving active purchases and subscription change.
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.
Designing a subscription screen
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:
- A heading.
- Buttons set to start the purchase process. These buttons guide the user about the subscription options’ general details, such as the title and the cost, which is displayed in the local currency.
- Instructional text.
- A button to restore the previously made purchase. This element is a must for any app that features subscriptions or non-consumable purchases.
Tweaking the code to display product details
There are four products in our example:
- Two auto-renewable subscriptions ("premium_sub_month" and "premium_sub_year");
- A product that can’t be bought twice, or a non-consumable product (“unlock_feature”);
- A product that can be bought multiple times, or a consumable product (“coin_pack_large”).
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.
Starting the purchase process
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:
- Via acknowledgePurchase() on the client side;
- Via Product.Purchases.Acknowledge/Purchases.Subscriptions.Acknowledge on the backend side.
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.
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 canceled and refunded. So if it’s impossible to do on the backend and you keep getting the 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, hoping 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 :)