A Java Android In-App Purchasing v5+ Tutorial — Google Play
In-app purchase (IAP) is a way to sell digital content, services, and virtual or physical items within a mobile application. Google Play allows developers to integrate Google Play’s in-app billing system into their apps and manage the purchasing of digital goods or services within the app. Developers can securely offer one-time products, non-consumable items, and subscription-based services, and Android users can purchase items securely with their Google Play balance, credit card, or carrier billing.
In-app purchasing (IAP) consists of allowing Android users to purchase digital or virtual content within an Android application. This content may consist of additional features, or virtual items such as coins, gems or other forms of currency used in the game. For example, a user may spend real money to purchase these coins or gems, which can be used to purchase special in-game items or abilities. There are two types of IAP:
- Consumable products: These refer to virtual items that can be purchased, used and then depleted or used up. An example of a consumable product would be coins, gems or any other form of in-game currency.
Examples of consumable products available on Android’s Google Play store include:
1. Mobile Phone top-ups
2. In-app purchases
3. Movie rentals
4. Music downloads
5. DLC content
6. Subscription services
7. Gaming micro-transactions
8. E-books
9. Magazine subscriptions
10. App-based payments
2. Non-consumable products: These are items that are purchased once and are then available for use indefinitely. An example of a non-consumable item would be a special outfit or character customization.
Android in-app subscriptions are a way for developers to monetize their mobile applications. They allow users to purchase access to premium features or content, such as ad-free play or additional content within an application. The Android platform provides an API that developers can use to create and manage subscription billing, entitlements, and features within their applications. With Android In-App subscriptions, developers can create subscription plans and offer customers a variety of pricing options. This helps them to effectively monetize their applications, generate additional revenue, and gain more exposure for their products.
Creating the InAppPurchase Project
Adding Libraries to the Project
Before we start writing code, some libraries need to be added to the project build configuration, one of which is the standard Android billing client library. Later in the project, we will also need to use the ImmutableList class which is part of Google’s Guava Core Java libraries. Add these libraries now by modifying the Gradle Scripts -> build.gradle (Module: InAppPurchase.app) file with the following changes:
dependencies {
implementation 'com.android.billingclient:billing:5.1.0'
}
Creating an In-App Product
With the app selected in the Play Console, scroll down the list of options in the left-hand panel until the Monetize section comes into view. Within this section, select the In-app products option listed under Products as shown in
On the In-app products page, click on the Create product button:
On the new product screen, enter the following information before saving the new product:
- Product ID: one_button_click
- Name: A Button Click
- Description: This is a test in-app product that allows a button to be clicked once.
- Default price: Set to the lowest possible price in your preferred currency.
Enabling License Testers
When testing in-app billing it is useful to be able to make test purchases without spending any money. This can be achieved by enabling license testing for the internal track testers. License testers can use a test payment card when making purchases so that they are not charged.
Within the Play Console, return to the main home screen and select the Setup -> License testing option:
Figure 84–4
Within the license testing screen, add the testers that were added for the internal testing track, change the License response setting to RESPOND_NORMALLY, and save the changes:
Now that both the app and the in-app product have been set up in the Play Console, we can start adding code to the project.
Initializing the Billing Client
Edit the MainActivity.java file and make the following changes to begin implementing the in-app purchase functionality:
package com.bayram.inappexample;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
Button btn_nonconsumeable,btn_consumeable,btn_subscription;
Intent intent = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init(){
btn_nonconsumeable = this.findViewById(R.id.btn_nonconsumeable);
btn_consumeable = this.findViewById(R.id.btn_consumeable);
btn_subscription = this.findViewById(R.id.btn_subscription);
btn_nonconsumeable.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
intent = new Intent(MainActivity.this, NonConsumable.class);
startActivity(intent);
}
});
btn_consumeable.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
intent = new Intent(MainActivity.this, Consumable.class);
startActivity(intent);
}
});
btn_subscription.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
intent = new Intent(MainActivity.this, Subscription.class);
startActivity(intent);
}
});
}
}
Consumable, NonConsumable, and Subscription Classes
Consumable, NonConsumable variables :
...........
private final String PRODUCT_PREMIUM = "lifetime";
private final String NoAds = "NoAds";
private ArrayList<String> purchaseItemIDs = new ArrayList<String>() {{
add(PRODUCT_PREMIUM);
add(NoAds);
}};
private String TAG = "iapSample";
private BillingClient billingClient;
Button btn_premium, btn_restore;
TextView tv_status;
.................
Subscription variables :
...........
private static String PRODUCT_PREMIUM = "yearly";
private static String PRODUCT_MONTHLY = "monthly";
private BillingClient billingClient;
Button btn_premium, btn_restore;
TextView tv_status;
private ArrayList<String> purchaseItemIDs = new ArrayList<String>() {{
add(PRODUCT_PREMIUM);
add(PRODUCT_MONTHLY);
}};
private String TAG = "iapSample";
.................
Launching the Purchase Flow
When the user clicks the buy button, a method named LaunchPurchaseFlow() will be called to start the purchase process. We can now add this method as follows:
Consumable, NonConsumable:
void LaunchPurchaseFlow(ProductDetails productDetails) {
ArrayList<BillingFlowParams.ProductDetailsParams> productList = new ArrayList<>();
productList.add(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build());
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productList)
.build();
billingClient.launchBillingFlow(this, billingFlowParams);
}
Subscription:
void LaunchSubPurchase(ProductDetails productDetails) {
assert productDetails.getSubscriptionOfferDetails() != null;
ArrayList<BillingFlowParams.ProductDetailsParams> productList = new ArrayList<>();
productList.add(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(productDetails.getSubscriptionOfferDetails().get(0).getOfferToken())
.build()
);
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productList)
.build();
billingClient.launchBillingFlow(this, billingFlowParams);
}
Handling Purchase Updates
The results of the purchase process will be reported to the app via the PurchaseUpdatedListener that was assigned to the billing client during the initialization phase. Add this handler now as follows:
Consumable, NonConsumable:
void handlePurchase(Purchase purchases) {
if (!purchases.isAcknowledged()) {
billingClient.acknowledgePurchase(AcknowledgePurchaseParams
.newBuilder()
.setPurchaseToken(purchases.getPurchaseToken())
.build(), billingResult -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (String pur : purchases.getProducts()) {
if (pur.equalsIgnoreCase(PRODUCT_PREMIUM)) {
Log.d("TAG", "Purchase is successful");
tv_status.setText("Yay! Purchased");
//Calling Consume to consume the current purchase
// so user will be able to buy same product again
ConsumePurchase(purchases);
}
}
}
});
}
}
Subscription:
void verifySubPurchase(Purchase purchases) {
if (!purchases.isAcknowledged()) {
billingClient.acknowledgePurchase(AcknowledgePurchaseParams
.newBuilder()
.setPurchaseToken(purchases.getPurchaseToken())
.build(), billingResult -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (String pur : purchases.getProducts()) {
if (pur.equalsIgnoreCase(PRODUCT_PREMIUM)) {
Log.d("TAG", "Purchase is successful" + pur);
tv_status.setText("Yay! Purchased");
}
}
}
});
}
}
Consuming the Product
With the user now able to click on the “consume” button, the next step is to make sure the product is consumed so that only one click can be performed before another button click is purchased. This requires that we now write the consumePurchase() method:
Consumable, NonConsumable:
//This function will be called in handlepurchase() after success of any consumeable purchase
void ConsumePurchase(Purchase purchase) {
ConsumeParams params = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(params, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String s) {
Log.d("TAG", "Consuming Successful: "+s);
tv_status.setText("Product Consumed");
}
});
}
Testing the App
Before we can test the app we need to upload this latest version to the Play Console. As we already have version 1 uploaded, we first need to increase the version number in the build.gradle file:
defaultConfig {
applicationId "com.bayram.inappexample"
minSdk 24
targetSdk 32
versionCode 2
versionName "2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}