Twitter LogoFacebook Logo
In-App Purchases
Selling In-App products
By: King

Hello, in this tutorial, I will show you how to use the Google Play Billing Library to start selling IN-APP products on your apps.

We will go over:

Add IN APP products to your app from the Google Play Console.

Launch a Purchase Flow UI to allow users purchase the item

Verify the purchase with a Back-end server using Firebase Cloud Functions and the Firestore Database

Acknowledge the purchase to finalize and receive payments

How to unlock the product so the user purchase the item again

Download Source Code to Project:

https://codeible.com/coursefiles/googleplayinapp

Adding the Google Play Billing Library

To begin, go to the Manifests file and add the INTERNET permission.

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

Then go to the app’s build.gradle file, implement the Google Play Billing Library, and sync the project.

dependencies {
    ...
    def billing_version = "4.0.0"
    implementation "com.android.billingclient:billing:$billing_version"
}

Setup for Testing

Go to the Build tab at the top and Generate a Signed App Bundle.

Image from Codeible.com

Once the process is complete, go to the Google Play Console and set up for testing. Go to play.google.com/console. 

https://play.google.com/console

On the left, under “Setup,” go to “License testing” and add your testing account.

Image from Codeible.com

Then go to the “All apps” page and click on the “Create app” button to create the console for your app. 

Image from Codeible.com

When you’re in your app’s console page, scroll down until you find the Internal testing tab on the left. 

Image from Codeible.com

Click on the “Create new release“ button and upload the app bundle.

Image from Codeible.com

When the upload is finished, scroll down and give the release a name. Save the changes, click on “Review release,” and then ”Start rollout to Internal testing”.  

Image from Codeible.com

When the release is created, click on the Testers tab and add the testing account that you added earlier in License Testing. 

Image from Codeible.com

The next step we need to do to, is to have an emulator that supports Google Play. Open the Android Virtual Device Manager. 

If your emulators do not have the Play Store icon next to it, you’ll need to create one that does.

Image from Codeible.com

Click on the "Create Virtual Device..." button and select the emulator with the Play Store icon.

Image from Codeible.com

Opt-in as a Tester

Start the emulator. 


Open the Google Play App.

Sign in with the testing account.

Image from Codeible.com

The last step we need to do is to opt-in as a tester. 


Go back to your app’s console page. In the Internal testing tab, scroll down until you see the section call "How testers join your test".

Image from Codeible.com

Click on the ”Copy Link.” 

Image from Codeible.com

Open the Chrome app on the Emulator. Paste the link in the address bar and then accept the invite to test the app. 

Image from Codeible.com

Adding In-App Products

To add IN-APP products to your app, go to your app’s console page and scroll down until you see the Monetize section on the left. 


Under “Products,” click on “In-app products.” 

Image from Codeible.com

Click on the “Create product” button on the right.

Image from Codeible.com

On this screen, we can enter an ID, Name, Description and Price for the item. 

The Product ID is a unique name for the product. It cannot be changed or reused in another item once it is created. 

The name and description are used to describe the item so users would know what they’ll be getting.

For the price, click on “Set price” and enter a price for the item. 

Click on “Apply prices,” “Save,” and then “Activate“ to complete the process.

If we go back to the In-app products page, the new item should be there. 

Image from Codeible.com

Repeat the steps one more time so you’ll have at least 2 products for this tutorial. 

Displaying the Items

Now that we have some items, let’s see how we can display in our app. 


Go to the Main Activity file of your project. Declare a BillingClient variable and use the newBuilder() and build() methods from BillingClient to create the object. 

private BillingClient billingClient;

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        billingClient = BillingClient.newBuilder(this).build();
}

The BillingClient object is used to communicate with the Google Play’s billing system. We can use it to get all the information from the products list.

Before we can connect to Google Play, it is required by us to support pending purchases. Call enablePendingPurchases() right before we build the client and then add a PurchasesUpdatedListener() so we can handle all the incoming purchases from the user.

 billingClient = BillingClient.newBuilder(this)
    .enablePendingPurchases()
    .setListener(new PurchasesUpdatedListener() {
          @Override
           public void onPurchasesUpdated(@NonNull BillingResult billingResult, 
                       @Nullable List<Purchase> list) {
                        
           }
     })
.build();

Now that we have our BillingClient, create a method call connectToGooglePlayBilling(). 


Inside the method, take the BillingClient and call startConnection().

private void connectToGooglePlayBilling(){
    billingClient.startConnection();
}

It takes in a BillingClientStateListener that is used to check if we have connected to the Google Play Billing System.

billingClient.startConnection(
    new BillingClientStateListener() {
       @Override
       public void onBillingServiceDisconnected() {

       }

       @Override
       public void onBillingSetupFinished(@NonNull BillingResult billingResult) {

       }
});

It is advisable that we restart the connection if we have been disconnected. Call the connectToGooglePlayBilling() method inside the onBillingServiceDisconnected() callback to reconnect. 

@Override
public void onBillingServiceDisconnected() {
      connectToGooglePlayBilling();
}

The onBillingSetupFinished() callback is used to let us know what the current connection state we’re in. If the response code we received from the billingResult object is OK, it means we have connected to the Google Play Billing System.

@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {

      if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {

      }

}

To get the products that we have added in the Google Play Console, create a method call getProductDetails() and then call it inside the onBillingSetupFinished() callback.

private void getProductInformation(){

}

Inside the getProductDetails() method, create a List call productIds and add the product id for each product you have in the Google Play Console. 


For now, I only want to show one item so I will just add 1.

List<String> productIds = new ArrayList<>();
productIds.add("skill_upper_cut");

Once you have the list of products ids, call querySkuDetailsAsync() from the billingClient to actually get the product information.

billingClient.querySkuDetailsAsync()

It takes 2 arguments, the first asks for a SkuDetailsParams. 


We use it to generate a query to get the product details using the list of product ids we have.

Declare a SkuDetailsParams variable call getProductDetailsQuery and call the newBuilder() and build() methods from SkuDetailsParams to create the object.

SkuDetailsParams getProductDetailsQuery = SkuDetailsParams.newBuilder().build();

Call setSkusList() to add the list and call setType() to set the type. 

SkuDetailsParams getProductDetailsQuery =
      SkuDetailsParams
         .newBuilder()
         .setSkusList(productIds)
         .setType(BillingClient.SkuType.INAPP)
         .build();

Then insert the query object into the querySkuDetailsAsync() method. 

billingClient.querySkuDetailsAsync(getProductDetailsQuery, );

For the second argument, it takes in a SkuDetailsResponseListener that we can use to access the results from the query.

The list object represents the products information we requested.

billingClient.querySkuDetailsAsync(
     getProductDetailsQuery,
     new SkuDetailsResponseListener() {
           @Override
            public void onSkuDetailsResponse(
                 @NonNull BillingResult billingResult,
                 @Nullable List<SkuDetails> list) {
            }
     });

Now that we retrieved the products’ information we can display it so users can see it. 


Normally, you’ll use a Recycler View or a Card View to display the items but since we only have 1 item, we’ll use a TextView to display the item’s name and a button to display the price. 

Go to MainAcitivty XML Layout File and add a TextView and Button inside. Set the constraints so that the button is on the right and the TextView is on the left.  

Image from Codeible.com

Rename the TextView id to itemName and the button to itemPrice.

Image from Codeible.com

Go back to the MainActivity file. In the SkuDetailsReponse() callback, create a reference to the TextView and Button.

TextView itemNameTextView = findViewById(R.id.itemName);
Button itemPriceButton = findViewById(R.id.itemPrice);

Since we only have 1 item, create a single SkuDetails variable and get the first item in the list. Then set the text of the TextView by getting the title and the Button by getting the price. 

SkuDetails itemInfo = list.get(0);
itemNameTextView.setText(itemInfo.getTitle());
itemPriceButton.setText(itemInfo.getPrice());

If we run the app, we should see the item.

Image from Codeible.com

Launching the Purchase Flow

Now that we have a way for users to see the item. We want them to be able to purchase it. 


Add an onClickListener to the price button.

itemPriceButton.setOnClickListener(
      new View.OnClickListener() {
             @Override
             public void onClick(View view) {

             }
      });

We want to launch the Purchase Flow UI so users can choose how to pay when they click on the button. 


Grab the billingClient and call launchBillingFlow(). 

It takes 2 parameters. a context, and a BillingFlowParams object. 

Use the newBuilder() and build() methods from BillingFlowParams to create the one. 


Then setSkuDetails() to add the item details before building it.

 Activity activity = this;
....

itemPriceButton.setOnClickListener(
        new View.OnClickListener() {
             @Override
             public void onClick(View view) {
                   billingClient.launchBillingFlow(
                          activity,
                          BillingFlowParams
                            .newBuilder()
                            .setSkuDetails(itemInfo)
                            .build());
             }
         }
);

If we run the app, and click on the item, a purchase flow UI should come up.

Image from Codeible.com

Verifying the Purchase with Back-end

Now let’s see how we can verify the purchase when the user pays for the item.


Whenever someone purchases an item, it will bring us back to the onPurchasesUpdated() callback. The billingResult object returns the status of all the purchases made and the purchase list represents all the purchases that were made by the user at the time.

This is where we need place the code to verify the purchase through a back-end server and acknowledge the payment to receive the money. 

 @Override
public void onPurchasesUpdated(
       @NonNull BillingResult billingResult, 
       @Nullable List<Purchase> list) {
            
        }
});

First make sure that the there was a successful purchase. Check if we got an OK response code and then check if the purchase list is not empty inside the onPurchasesUpdated() callback.

if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && 

    list != null) {

}

To verify if the purchases are real requires 2 steps.


The first step is to make sure that the item is in the PURCHASED state and was not acknowledged before. 

Use the For loop to iterate through all the purchases that are coming in. Then use the if statement to validate the state and acknowledgement.

if(billingResult.getResponseCode() == ...) {
          for(Purchase purchase: list) {
                if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED &&
                             !purchase.isAcknowledged()) {
                                             
                }
          }
}

The second step is to verify the purchase using a back-end server. 


We’ll need to get the purchase token for each purchase, send it to our back-end server, and then check to see if the token was never used. If the token was never used, it means it is a valid token. 

When we verified that the token is valid, we can store that purchase information in an online database and then send a message back to the app to notify the user.

For this part, we’ll use Firebase Cloud Functions as the back-end server and Firestore as the database.

Begin by creating a Cloud Functions project.  

Creating a Cloud Functions Project

Go to console.firebase.google.com. Sign in with your Google account and create a project. If you do not have a Google account, go to accounts.google.com/signup to create an account and then go back to Firebase.

 console.firebase.google.com

accounts.google.com/signup

Once you have created a project, make sure you have node and the firebase-tools installed on your computer.


Run node -v in the terminal to get  the version of node you have installed and the firebase - -version for firebase-tools. 

node -v

firebase --version

If you do not have them installed, go to nodejs.org and follow the steps to install node.

nodejs.org

For firebase-tools, run npm install -g firebase-tools after you have node installed.

npm install -g firebase-tools

When you have node and firebase-tools installed, create a folder where it is easy to locate. Open a terminal for that directory and log into firebase. 

firebase login

Afterwards, initialize Firebase Cloud Functions using the firebase init command. 

firebase init

Select the Firebase project you have for the app and continue. 


Select “Yes” to the install the dependencies and continue.

Once Cloud Functions is set, open the project in a code editor. Expand the functions folder and go to the index.js file. 


In here, we can define our validation functions. 

Image from Codeible.com

Type exports, then use the dot operator to name the function and assign a request handler function to it.

exports.verifyPurchases = functions.https.onRequest(
    (req,res) => {

    }
);

To store the token’s data that we’ll be receiving, create a JSON object call purchaseInfo. 

It should at least contain these 4 properties, purchaseToken, orderId, purchaseTime, and isValid.

To get the purchase token, grab the request object, call query, and then access the purchaseToken property. 

For the orderId and purchaseTime, we’ll do the same. 

For isValid, set it to false as the default value.

var purchaseInfo = {
   purchaseToken: req.query.purchaseToken,
   orderId: req.query.orderId,
   purchaseTime: req.query.purchaseTime,
   isValid: false
}

Now that we have the purchase information in a JSON object, we need to connect to Firestore and check if the token was used or not. 


Import the admin module and initialize the app. 

const functions = require("firebase-functions");
...
const admin = require("firebase-admin");
admin.initializeApp();


exports.verifyPurchases = functions.https.onRequest(
    (req,res) => {
        ...
    }
);

Inside the validate function, create an instance of Firestore by taking the admin object and calling the firestore() function.

var purchaseInfo = {
   ...
}

var firestore = admin.firestore();

To check if a token exists in Firestore, we just need to see if there is a document with the same token id as the current token. If there is no such document, it means it is a new token.  

Grab the Firestore object, call doc(), get(), and then(). 

Inside the doc() function, enter the path to the collection where you would store all your purchase information. Use purchaseToken as the ID of the document id.

For then(), pass in an anonymous function with a result parameter.

firestore.doc(`purchases/${purchaseInfo.purchaseToken}`)
             .get()
             .then((result) => {
              
              }
);

Result represents the document that was retrieve from Firestore. We can use the exists property to check if there was a document with the same purchase token. 

if(result.exists) {

}

If there is, send a the purchase information back to let the app know that it was not a valid purchase token.

if(result.exists) {
   res.send(purchaseInfo);
}

If it does not exist, we want to add the purchase information to the database. 

Set the isValid property from the purchaseInfo object to true.

Grab the firestore object, call doc(), set(), and then(). 

Inside the doc() function, pass in the same path to the document. 

For set, pass in the purchaseInfo object, and for then, pass in an anonymous function and send the updated purchase information back.

 if(result.exists) {
    ...
} else {
    purchaseInfo.isValid = true;
    firestore.doc(`purchases/${purchaseInfo.purchaseToken}`)
                 .set(purchaseInfo)
                 .then(() => {
                            res.send(purchaseInfo);
                  });
}

This is all we have to do to validate the purchase. 


The next step is to upload this function to Cloud Functions so we can use it. Open the terminal and run the deploy command. 

firebase deploy --only functions

Image from Codeible.com

Make sure your Firebase project is upgraded to the Blaze plan before executing the command or you will not be able to deploy the function.  

Once the deployment is completed, go to the Firebase console. In the Functions tab, you should see the function in the system.

Image from Codeible.com

Copy the Request URL for the function, and paste it inside your Android project so we have it. We’ll need to use it to send data back and forth between the server and the app.

The idea is to call the function in the server using the Request URL, and when the server is done doing whatever it needs, it’ll send message back to let us know whether or not the purchase was valid. 

Implementing Volley

To communicate with Cloud Functions using the Request URL, go to the app’s build gradle file and implement Volley. 

dependencies {
   ...
    def volley_version = "1.2.0"
    implementation "com.android.volley:volley:$volley_version"
}

Go back to the MainActivity file and create a new method call verifyPurchase() with a purchase object parameter.

private void verifyPurchase(Purchase purchase) {

}

Inside the method create a String variable for the Request URL. 

String requestUrl = "https://us-central1-playbillingtutorial.cloudfunctions.net/verifyPurchases"

If we take a look at Cloud Functions, we are trying to get the purchaseToken, purchaseTime, and orderId from an object call query. 

Image from Codeible.com

To add data to the query object, we need to add a question mark (?) symbol at the end of the URL to represent the start of the query parameter. For each set of data, we need to insert a key and value pair and separate each set with a ampersand (&)symbol.

String requestUrl = "https://us-central1-playbillingtutorial.cloudfunctions.net/verifyPurchases?" +
                "purchaseToken=" + purchase.getPurchaseToken() + "&" +
                "purchaseTime=" + purchase.getPurchaseTime() + "&" +
                "orderId=" + purchase.getOrderId();

Make sure that the name of each key is identical to what you defined in the JSON object in Cloud Functions.


Now that we have our URL set up, create a POST request using StringRequest from the Volley library. 

It takes 4 arguments. 

The first is the type of request we are trying to make, and we’re trying to make a POST request.  

The second argument is the Request URL.

The third argument is the response listener callback function which will be called when we receive a response back from the server.

The fourth argument is a callback for errors.

 StringRequest stringRequest = new StringRequest(
     Request.Method.POST,
     requestUrl,
     new Response.Listener<String>() {
         @Override
         public void onResponse(String response) {
                        
     },
     new Response.ErrorListener() {
          @Override
           public void onErrorResponse(VolleyError error) {

           }
     }
);

Now that we have our Request Setup, we need to send it to the server. 


Create a RequestQueue using newRequestQueue() from Volley and add the StringRequest to the queue.

StringRequest stringRequest = new StringRequest(...)

Volley.newRequestQueue(this).add(stringRequest);

Remember to call the verifyPurchase() method inside the  onPurchasesUpdated() callback.

for(Purchase purchase: list) {
   if(purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED &&
            !purchase.isAcknowledged()) {
               verifyPurchase(purchase);
   }
}

If we run the app, and purchase the item, it should be uploaded to Firestore. 

Go to the Firebase Console. Change the Firestore rules to true so we can read and write data. 

Image from Codeible.com

If we run the app, and purchase the item, it should be uploaded to Firestore.

Image from Codeible.com

If we try to purchase the item again, we’ll get an error. 


This is because all In-App purchase products are defaulted to one-time purchase only. In order the actually get the money for each purchase, we need to acknowledge it, either by calling acknowledgePurchase() or consumeAsync(). 

We use acknowledgePurchase() if the product is a one-time purchase item and we use consumeAsync() if the item is purchasable multiple times. 

Acknowledging the Purchases

Go back to the verifyPurchase() method. Inside the response listener callback, create a JSONObject using the response from the server. 

Then use an If statement to check if the isValid property is true. It if is, we can start acknowledging the purchase.

try {

  JSONObject purchaseInfoFromServer = new JSONObject(response);

  if(purchaseInfoFromServer.getBoolean("isValid")) {

  }

} catch (Exception err) {

}

To use acknowledgePurchase(), create a AcknowledgePurchaseParams object using newBuilder() and build() from AcknowledgePurchaseParams. 


Then add the purchase token of the purchase you want to acknowledge using setPurchaseToken() right before you build the params object.

AcknowledgePurchaseParams acknowledgePurchaseParams = 
                             AcknowledgePurchaseParams
                            .newBuilder()
                            .setPurchaseToken(purchase.getPurchaseToken())
                            .build();

Next, grab the billingClient and call acknowledgePurchase(). 


Pass in the purchase params and add a AcknowledgePurchaseResponseListener.

billingClient.acknowledgePurchase(
    acknowledgePurchaseParams,
    new AcknowledgePurchaseResponseListener() {
           @Override
           public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
                if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                       Toast.makeText(activity, "Consumed!", Toast.LENGTH_LONG).show();
                }
           }
    }
);

Inside the response listener, check if the result was OK. If it is, we need to put the logic to notify and give the item to the user. For now, we’ll use a Toast message. 

If we run the app and purchase the item again, it will get acknowledged. If you are still getting the “You already own this item” error, wait a few minutes for it to reset and try again.

Unlocking the Item

Now let’s see how we can acknowledge consumables. Go back to the getProductDetails() method and replace the old item with a new one.

private void getProductDetails(){

        List<String> productIds = new ArrayList<>();
        productIds.add("small_potion");

        ...
}

To acknowledge consumables, we need to use consumeAsync() instead of acknowledgePurchase().

Go back to the verifyPurchase() method. Delete the acknowledgePurchase code and call the consumeAsync() method from the billingClient.

billingClient.consumeAsync()

It takes 2 arguments. The first is a ConsumeParams object. Create one by using the newBuilder() and build() methods from ConsumeParams. Then add the purchase token by calling setPurchaseToken().

The second argument is for the ConsumeResponseListener.

ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();

billingClient.consumeAsync(
        consumeParams,
        new ConsumeResponseListener() {
            @Override
            public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String s) {

            }
        }
);

Inside the response listener, check if the result was OK. If it is, we need to put the logic to notify and give the item to the user. For now, we’ll use a Toast message again.

if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
       Toast.makeText(activity, "Consumed!", Toast.LENGTH_LONG).show();
}

If we run the app, we can purchase the item multiple times.

Handling Purchases That Were Made While the App is Closed

This is all we need to do to verify the purchases. But as best practice, we should not rely solely on the onPurchasesUpdated() callback. This is because the callback will only be available when the app is opened. 


To handle purchases that were made while the app was closed, we need to override the onResume() method.

@Override
protected void onResume() {
     super.onResume();
}

The idea is to retrieve all the purchases when the user returns to the app and then we’ll verify each purchase accordingly. 

Inside the method, grab the BillingClient and call queryPurchasesAsync(). 

The queryPurchasesAsync() method returns the purchases made by the user. It takes 2 arguments.

The first argument is the type of product we want to get. 

The second argument is for a purchasesResponseListener() which give us access to the result of the query.

billingClient.queryPurchasesAsync(
   BillingClient.SkuType.INAPP,
   new PurchasesResponseListener() {
        @Override
        public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {

          }
    }
);

Use the BillingResult object and check if the status was OK. Inside the if statement, iterate through all the purchases that were made and check if the state was purchased and not yet acknowledged. If it was not acknowledged, we want to verify the purchase.

if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
     for(Purchase purchase: list) {
          if(purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED &&
                                !purchase.isAcknowledged()) {
                    verifyPurchase(purchase);
          }
     }
 }

That is all for this tutorial. If you find this helpful, please give it a like, share, and subscribe to support the channel.


Sign In