HOW TO MAKE IN APP PURCHASE MULTIPLE SUBSCRIPTION IN ANDROID USING GOOGLE PLAY BILLING LIBRARY
Please Subscribe Youtube| Like Facebook | Follow Twitter
Introduction
In this article we will learn how to integrate and make Multiple In App Subscription (weekly,monthly,yearly) using google play billing library.
Tips
Article | Java | Kotlin |
For Single Non-consumable one-time product | link | link |
For Multiple Non-consumable one-time product | link | link |
For Single Consumable one-time product | link | link |
For Multiple Consumable one-time product | link | link |
For Single In App Subscriptions | link | link |
For Multiple In App Subscriptions | link | link |
Our Example
In our example we have a listview which shows list of Subscription items and on click of particular item from list allows user to purchase that Subscription item from list. When the particular item is purchased/subscribed then we will acknowledge that purchase item as well as save its value in preference and refresh the list.
On every app start we will check purchase/subscribe status of items from Google Play Store Cache and reflect necessary changes accordingly.
Subscriptions on Google Play renew automatically unless user unsubscribe. See here regarding Subscription renewal and cancellation. When user not renews subscription and cancels it then on every app launch we will update subscriptions status in preference
Google Play Store Cache works best on both offline as well as online. When there is no internet Cache will contain last updated purchase status of item. When internet become available Play Store Cache will be updated automatically with latest purchase status of item. Google Play Store Cache updated roughly every 24 hours or immediately on every purchase.



Requirements:
- Android Project/App to which In App Purchase Item to be added
- Google play console account
Note: In order to create in app product items in Google Console or perform real test of in app purchase item in your app, you first need to Integrate Google Play Library and add billing permission in your project and then upload that apk on Google play console and publish it any track (Internal test track (recommended), Closed track, Open track, Production track). Therefore we will perform these actions first.
Steps
Follow below steps
1 Integrate Google Play Library in your project
2 Create an application and fill Store listing in Google play console
3 Upload and publish your app
4 Create Subscription items “s1” “s2” “s3” in Google play console
5 Code in app purchase flow logic.
6 Run and Test the application
Step 1 to 3 already explained in this article so follow these steps from there.
4 Create Subscription items in Google play console
After releasing app go to Monetize -> Products -> Subscriptions
Click on create subscription and fill necessary info then save and activate the Subscription items. Create three Subscription items (weekly,monthly,yearly) and give them id “s1”, “s2”, “s3” respectively.
Subscription option includes Free Trail: Disabled, Introductory price: None, Grace Period: None, Resubscribe: Enabled for all.



After creating subscription item will be displayed inside Manage Subscription.

We have used Default Subscription Setting which are as follows
Account hold : Enabled , Pause : Enabled , Subscription restore : Enabled and Free Trail Limit : One Across All Subscriptions.

5 Code in app purchase flow logic.
First make sure you have added Google Play Billing Library dependency and billing permission, which was described in step 1.
For multiple purchase/subscribe items we have to make arraylist of purchase item ids and loop through purchase list and make necessary changes. Note that we have assigned preference key and product/subscribe item id same. You can check these in whole code of MainActivity.
First Add listview in activity_main.xml Layout.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
In order to make In-App purchase our MainActivity needs to implements PurchasesUpdatedListener interface and override onPurchasesUpdated() method which will be called every time to get notifications for purchases updates. Both purchases initiated by your app and the ones initiated outside of your app will be reported here.
We also need to create BillingClient Object and set listener to it so that we can communicate with Google Play Billing Service.
public class MainActivity extends AppCompatActivity implements PurchasesUpdatedListener {
private BillingClient billingClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases().setListener(this).build();
}
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
//we will code subscription result logic later
}
}
Now on every item click from list we will initiate purchase flow of that particular item.
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
if(getSubscribeItemValueFromPref(subcribeItemIDs.get(position))){
Toast.makeText(getApplicationContext(),subcribeItemIDs.get(position)+" is Already Subscribed",Toast.LENGTH_SHORT).show();
//selected item is already purchased/subscribed
return;
}
//initiate purchase on selected product/subscribe item click
//check if service is already connected
if (billingClient.isReady()) {
initiatePurchase(subcribeItemIDs.get(position));
}
//else reconnect service
else{
billingClient = BillingClient.newBuilder(MainActivity.this).enablePendingPurchases().setListener(MainActivity.this).build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
initiatePurchase(subcribeItemIDs.get(position));
} else {
Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
}
}
@Override
public void onBillingServiceDisconnected() {
}
});
}
}
});
In initiatePurchase() method we have passed selected product id and specified product type as SUBS. Then we have called querySkuDetailsAsync() method which will query our product from Play console. After querying we will call launchBillingFlow() method which will display purchase dialog. Result of purchase dialog will be reported inside onPurchasesUpdated() method which we will code next.
private void initiatePurchase(final String PRODUCT_ID) {
List<String> skuList = new ArrayList<>();
skuList.add(PRODUCT_ID);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SUBS);
BillingResult billingResult = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS);
if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (skuDetailsList != null && skuDetailsList.size() > 0) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetailsList.get(0))
.build();
billingClient.launchBillingFlow(MainActivity.this, flowParams);
} else {
//try to add item/product id "s1" "s2" "s3" inside subscription in google play console
Toast.makeText(getApplicationContext(), "Subscribe Item " + PRODUCT_ID + " not Found", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getApplicationContext(),
" Error " + billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
}
}
});
}
else{
Toast.makeText(getApplicationContext(),
"Sorry Subscription not Supported. Please Update Play Store", Toast.LENGTH_SHORT).show();
}
}
In onPurchasesUpdated() method we will check purchase result and handle it accordingly.
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
//if item newly purchased
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
handlePurchases(purchases);
}
//if item already purchased then check and reflect changes
else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
Purchase.PurchasesResult queryAlreadyPurchasesResult = billingClient.queryPurchases(SUBS);
List<Purchase> alreadyPurchases = queryAlreadyPurchasesResult.getPurchasesList();
if(alreadyPurchases!=null){
handlePurchases(alreadyPurchases);
}
}
//if purchase cancelled
else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
Toast.makeText(getApplicationContext(),"Purchase Canceled",Toast.LENGTH_SHORT).show();
}
// Handle any other error msgs
else {
Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
}
}
In handlePurchases() method we will handle, verify and acknowledge purchase/subscription . After the subscription, acknowledgement is necessary because failure to properly acknowledge purchase will result in purchase being refunded. After the acknowledgement we will save subscribe value in preference and refresh list.
This method also toast user to complete transaction if purchase status is pending. In case of unspecified purchase state we save false value of that product in preference.
void handlePurchases(List<Purchase> purchases) {
for(Purchase purchase:purchases) {
final int index=subcribeItemIDs.indexOf(purchase.getSku());
//purchase found
if(index>-1) {
//if item is purchased
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
{
if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
// Invalid purchase
// show error to user
Toast.makeText(getApplicationContext(), "Error : Invalid Purchase", Toast.LENGTH_SHORT).show();
continue;//skip current iteration only because other items in purchase list must be checked if present
}
// else purchase is valid
//if item is purchased/subscribed and not Acknowledged
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams,
new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
//if purchase is acknowledged
//then saved value in preference
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),true);
Toast.makeText(getApplicationContext(), subcribeItemIDs.get(index)+" Item Subscribed", Toast.LENGTH_SHORT).show();
notifyList();
}
}
});
}
//else item is purchased and also acknowledged
else {
// Grant entitlement to the user on item purchase
if(!getSubscribeItemValueFromPref(subcribeItemIDs.get(index))){
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),true);
Toast.makeText(getApplicationContext(), subcribeItemIDs.get(index)+" Item Subscribed.", Toast.LENGTH_SHORT).show();
notifyList();
}
}
}
//if purchase is pending
else if( purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
{
Toast.makeText(getApplicationContext(),
subcribeItemIDs.get(index)+" Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
}
//if purchase is refunded or unknown
else if( purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
{
//mark purchase false in case of UNSPECIFIED_STATE
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),false);
Toast.makeText(getApplicationContext(), subcribeItemIDs.get(index)+" Purchase Status Unknown", Toast.LENGTH_SHORT).show();
notifyList();
}
}
}
}
For verification we have used client side signature verification, which will be performed with in app (less secure). However if you can afford server then you should do verification of purchase through your server (more secure).
In client side verification you need to add your developer’s public key of app.
For old Play Console Design To get key go to Developer Console > Select your app > Development Tools > Services & APIs.

For new Play Console Design To get key go to Developer Console > Select your app > Monetize > Monetization setup

Then paste that key to String variable base64key in below method.
/**
* Verifies that the purchase was signed correctly for this developer's public key.
* <p>Note: It's strongly recommended to perform such check on your backend since hackers can
* replace this method with "constant true" if they decompile/rebuild your app.
* </p>
*/
private boolean verifyValidSignature(String signedData, String signature) {
try {
// To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
String base64Key = "Add Your Key here";
return Security.verifyPurchase(base64Key, signedData, signature);
} catch (IOException e) {
return false;
}
}
Code of Security.java is at whole code section
Up to that point our purchase logic is complete. Now On every app start we will check subscription status of items from Google Play Store Cache using getPurchasesList() method and reflect necessary changes accordingly because if user already subscribed item previously and reinstalls the app or switch to another device or cancelled and not renewed subscription therefore we should store updated subscription status in user preference. After querying we will call handlePurchases() method which will make necessary changes accordingly. Arraylist purchaseFound is used to loop through each purchase items and update their values in preferences.
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(SUBS);
List<Purchase> queryPurchases = queryPurchase.getPurchasesList();
if(queryPurchases!=null && queryPurchases.size()>0){
handlePurchases(queryPurchases);
}
//check which items are in purchase list and which are not in purchase list
//if items that are found add them to purchaseFound
//check status of found items and save values to preference
//item which are not found simply save false values to their preference
//indexOf return index of item in purchase list from 0-2 (because we have 3 items) else returns -1 if not found
ArrayList<Integer> purchaseFound =new ArrayList<Integer> ();
if(queryPurchases!=null && queryPurchases.size()>0){
//check item in purchase list
for(Purchase p:queryPurchases){
int index=subcribeItemIDs.indexOf(p.getSku());
//if purchase found
if(index>-1)
{
purchaseFound.add(index);
if(p.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
{
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),true);
}
else{
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),false);
}
}
}
//items that are not found in purchase list mark false
//indexOf returns -1 when item is not in foundlist
for(int i=0;i < subcribeItemIDs.size(); i++){
if(purchaseFound.indexOf(i)==-1){
saveSubscribeItemValueToPref(subcribeItemIDs.get(i),false);
}
}
}
//if purchase list is empty that means no item is not purchased/Subscribed
//Or purchase is refunded or canceled
//so mark them all false
else{
for( String purchaseItem: subcribeItemIDs ){
saveSubscribeItemValueToPref(purchaseItem,false);
}
}
}
}
@Override
public void onBillingServiceDisconnected() {
}
});
Below is the logic of managing multiple items through arraylist and notifyList() method is used to reflect changes in list when it changes.
//note add unique product ids
//use same id for preference key
private static ArrayList<String> subcribeItemIDs = new ArrayList<String>() {{
add("s1");
add("s2");
add("s3");
}};
private static ArrayList<String> subscribeItemDisplay = new ArrayList<String>();
private SharedPreferences getPreferenceObject() {
return getApplicationContext().getSharedPreferences(PREF_FILE, 0);
}
private SharedPreferences.Editor getPreferenceEditObject() {
SharedPreferences pref = getApplicationContext().getSharedPreferences(PREF_FILE, 0);
return pref.edit();
}
private boolean getSubscribeItemValueFromPref(String PURCHASE_KEY){
return getPreferenceObject().getBoolean(PURCHASE_KEY,false);
}
private void saveSubscribeItemValueToPref(String PURCHASE_KEY,boolean value){
getPreferenceEditObject().putBoolean(PURCHASE_KEY,value).commit();
}
private void notifyList(){
subscribeItemDisplay.clear();
for(String p:subcribeItemIDs){
subscribeItemDisplay.add("Subscribe Status of "+p+" = "+getSubscribeItemValueFromPref(p));
}
arrayAdapter.notifyDataSetChanged();
}
Whole Code
App level build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.programtown.example"
minSdkVersion 17
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.android.billingclient:billing:3.0.1'
}
Manifest File
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.programtown.example">
<uses-permission android:name="com.android.vending.BILLING" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
MainActivity.java
package com.programtown.example;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.android.billingclient.api.BillingClient.SkuType.SUBS;
public class MainActivity extends AppCompatActivity implements PurchasesUpdatedListener {
public static final String PREF_FILE= "MyPref";
//note add unique product ids
//use same id for preference key
private static ArrayList<String> subcribeItemIDs = new ArrayList<String>() {{
add("s1");
add("s2");
add("s3");
}};
private static ArrayList<String> subscribeItemDisplay = new ArrayList<String>();
ArrayAdapter<String> arrayAdapter;
ListView listView;
private BillingClient billingClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView=(ListView) findViewById(R.id.listview);
// Establish connection to billing client
//check purchase status from google play store cache on every app start
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases().setListener(this).build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(SUBS);
List<Purchase> queryPurchases = queryPurchase.getPurchasesList();
if(queryPurchases!=null && queryPurchases.size()>0){
handlePurchases(queryPurchases);
}
//check which items are in purchase list and which are not in purchase list
//if items that are found add them to purchaseFound
//check status of found items and save values to preference
//item which are not found simply save false values to their preference
//indexOf return index of item in purchase list from 0-2 (because we have 3 items) else returns -1 if not found
ArrayList<Integer> purchaseFound =new ArrayList<Integer> ();
if(queryPurchases!=null && queryPurchases.size()>0){
//check item in purchase list
for(Purchase p:queryPurchases){
int index=subcribeItemIDs.indexOf(p.getSku());
//if purchase found
if(index>-1)
{
purchaseFound.add(index);
if(p.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
{
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),true);
}
else{
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),false);
}
}
}
//items that are not found in purchase list mark false
//indexOf returns -1 when item is not in foundlist
for(int i=0;i < subcribeItemIDs.size(); i++){
if(purchaseFound.indexOf(i)==-1){
saveSubscribeItemValueToPref(subcribeItemIDs.get(i),false);
}
}
}
//if purchase list is empty that means no item is not purchased/Subscribed
//Or purchase is refunded or canceled
//so mark them all false
else{
for( String purchaseItem: subcribeItemIDs ){
saveSubscribeItemValueToPref(purchaseItem,false);
}
}
}
}
@Override
public void onBillingServiceDisconnected() {
}
});
arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, subscribeItemDisplay);
listView.setAdapter(arrayAdapter);
notifyList();
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
if(getSubscribeItemValueFromPref(subcribeItemIDs.get(position))){
Toast.makeText(getApplicationContext(),subcribeItemIDs.get(position)+" is Already Subscribed",Toast.LENGTH_SHORT).show();
//selected item is already purchased/subscribed
return;
}
//initiate purchase on selected product/subscribe item click
//check if service is already connected
if (billingClient.isReady()) {
initiatePurchase(subcribeItemIDs.get(position));
}
//else reconnect service
else{
billingClient = BillingClient.newBuilder(MainActivity.this).enablePendingPurchases().setListener(MainActivity.this).build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
initiatePurchase(subcribeItemIDs.get(position));
} else {
Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
}
}
@Override
public void onBillingServiceDisconnected() {
}
});
}
}
});
}
private void notifyList(){
subscribeItemDisplay.clear();
for(String p:subcribeItemIDs){
subscribeItemDisplay.add("Subscribe Status of "+p+" = "+getSubscribeItemValueFromPref(p));
}
arrayAdapter.notifyDataSetChanged();
}
private SharedPreferences getPreferenceObject() {
return getApplicationContext().getSharedPreferences(PREF_FILE, 0);
}
private SharedPreferences.Editor getPreferenceEditObject() {
SharedPreferences pref = getApplicationContext().getSharedPreferences(PREF_FILE, 0);
return pref.edit();
}
private boolean getSubscribeItemValueFromPref(String PURCHASE_KEY){
return getPreferenceObject().getBoolean(PURCHASE_KEY,false);
}
private void saveSubscribeItemValueToPref(String PURCHASE_KEY,boolean value){
getPreferenceEditObject().putBoolean(PURCHASE_KEY,value).commit();
}
private void initiatePurchase(final String PRODUCT_ID) {
List<String> skuList = new ArrayList<>();
skuList.add(PRODUCT_ID);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SUBS);
BillingResult billingResult = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS);
if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (skuDetailsList != null && skuDetailsList.size() > 0) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetailsList.get(0))
.build();
billingClient.launchBillingFlow(MainActivity.this, flowParams);
} else {
//try to add item/product id "s1" "s2" "s3" inside subscription in google play console
Toast.makeText(getApplicationContext(), "Subscribe Item " + PRODUCT_ID + " not Found", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getApplicationContext(),
" Error " + billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
}
}
});
}
else{
Toast.makeText(getApplicationContext(),
"Sorry Subscription not Supported. Please Update Play Store", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
//if item newly purchased
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
handlePurchases(purchases);
}
//if item already purchased then check and reflect changes
else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
Purchase.PurchasesResult queryAlreadyPurchasesResult = billingClient.queryPurchases(SUBS);
List<Purchase> alreadyPurchases = queryAlreadyPurchasesResult.getPurchasesList();
if(alreadyPurchases!=null){
handlePurchases(alreadyPurchases);
}
}
//if purchase cancelled
else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
Toast.makeText(getApplicationContext(),"Purchase Canceled",Toast.LENGTH_SHORT).show();
}
// Handle any other error msgs
else {
Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
}
}
void handlePurchases(List<Purchase> purchases) {
for(Purchase purchase:purchases) {
final int index=subcribeItemIDs.indexOf(purchase.getSku());
//purchase found
if(index>-1) {
//if item is purchased
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
{
if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
// Invalid purchase
// show error to user
Toast.makeText(getApplicationContext(), "Error : Invalid Purchase", Toast.LENGTH_SHORT).show();
continue;//skip current iteration only because other items in purchase list must be checked if present
}
// else purchase is valid
//if item is purchased/subscribed and not Acknowledged
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams,
new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
//if purchase is acknowledged
//then saved value in preference
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),true);
Toast.makeText(getApplicationContext(), subcribeItemIDs.get(index)+" Item Subscribed", Toast.LENGTH_SHORT).show();
notifyList();
}
}
});
}
//else item is purchased and also acknowledged
else {
// Grant entitlement to the user on item purchase
if(!getSubscribeItemValueFromPref(subcribeItemIDs.get(index))){
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),true);
Toast.makeText(getApplicationContext(), subcribeItemIDs.get(index)+" Item Subscribed.", Toast.LENGTH_SHORT).show();
notifyList();
}
}
}
//if purchase is pending
else if( purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
{
Toast.makeText(getApplicationContext(),
subcribeItemIDs.get(index)+" Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
}
//if purchase is refunded or unknown
else if( purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
{
//mark purchase false in case of UNSPECIFIED_STATE
saveSubscribeItemValueToPref(subcribeItemIDs.get(index),false);
Toast.makeText(getApplicationContext(), subcribeItemIDs.get(index)+" Purchase Status Unknown", Toast.LENGTH_SHORT).show();
notifyList();
}
}
}
}
/**
* Verifies that the purchase was signed correctly for this developer's public key.
* <p>Note: It's strongly recommended to perform such check on your backend since hackers can
* replace this method with "constant true" if they decompile/rebuild your app.
* </p>
*/
private boolean verifyValidSignature(String signedData, String signature) {
try {
//for old playconsole
// To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
//for new play console
//To get key go to Developer Console > Select your app > Monetize > Monetization setup
String base64Key = "Add your key here";
return Security.verifyPurchase(base64Key, signedData, signature);
} catch (IOException e) {
return false;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(billingClient!=null){
billingClient.endConnection();
}
}
}
Security.java
/*
* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.programtown.example;
import android.text.TextUtils;
import android.util.Base64;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
/**
* Security-related methods. For a secure implementation, all of this code should be implemented on
* a server that communicates with the application on the device.
*/
public class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns the verified
* purchase.
* @param base64PublicKey the base64-encoded public key to use for verifying.
* @param signedData the signed JSON string (signed, not encrypted)
* @param signature the signature for the data, signed with the private key
* @throws IOException if encoding algorithm is not supported or key specification
* is invalid
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData,
String signature) throws IOException {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey)
|| TextUtils.isEmpty(signature)) {
//Purchase verification failed: missing data
return false;
}
PublicKey key = generatePublicKey(base64PublicKey);
return verify(key, signedData, signature);
}
/**
* Generates a PublicKey instance from a string containing the Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
* @throws IOException if encoding algorithm is not supported or key specification
* is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) throws IOException {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
// "RSA" is guaranteed to be available.
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
String msg = "Invalid key specification: " + e;
throw new IOException(msg);
}
}
/**
* Verifies that the signature from the server matches the computed signature on the data.
* Returns true if the data is correctly signed.
*
* @param publicKey public key associated with the developer account
* @param signedData signed data from server
* @param signature server signature
* @return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
byte[] signatureBytes;
try {
signatureBytes = Base64.decode(signature, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
//Base64 decoding failed
return false;
}
try {
Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM);
signatureAlgorithm.initVerify(publicKey);
signatureAlgorithm.update(signedData.getBytes());
if (!signatureAlgorithm.verify(signatureBytes)) {
//Signature verification failed
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
// "RSA" is guaranteed to be available
throw new RuntimeException(e);
} catch (InvalidKeyException e) {
//Invalid key specification
} catch (SignatureException e) {
//Signature exception
}
return false;
}
}
6 Run and Test the application
Now Run your app and test in app purchase by using your gmail account that is associated with your Google Console developer account. Because by default, the only test account registered is the one that’s associated with your developer account. You can register additional test accounts by using the Google Play Console.







Conclusion
So in this article we have learnt how to integrate and make in app purchase of multiple subscriptions. If you liked the article then please share this site and article. Thanks.
NOTE:
Need Support?
References:
https://developer.android.com/google/play/billing/billing_overview
Please Subscribe Youtube| Like Facebook | Follow Twitter
Thank you so much for this hard work writing all of this and sharing your knowledge with us.
I am so Amazed of how many articles you did about Implementing the in app billing from google.
I noticed that you never made an article about having one subscription and one time payment in an app. I am trying to do this in my app and it seems kind of pain of the tooth especially when the override methods can take either SUB or INAPP. Do you have any example of an app that has both of them. Thanks in advance
I have not done both in single sample project.
But You can book order for personal support and implementation with latest library via Fiver for that.
https://www.fiverr.com/share/NEW6eN
thanks
Thank you for this tutorial. However, in my handlePurchases method, my index = itemIDs.indexOf is returning -1. I guess it should be from 0-2. What could be wrong? I wish I can attach an image.
this code is for finding purchased item of user from play console.
if user purchased it then index will be found otherwise will not be found.
please review my code again.
You can also book order for personal support and implementation with latest library via Fiver.
https://www.fiverr.com/share/NEW6eN
Thanks
I have reviewed the code many times and it keeps returning -1. Could it be because I using a test account? It returns a JSON of the purchase but the index keeps returning -1.
what billing library version you are using?
I think version 3 deprecated and not supported
For latest library version 5 sample paid code you can get it from here
https://programtown.com/android-google-in-app-purchase-subscription-download-example-source-code-google-play-billing-library-5-0-java-kotlin/
Hi,
Thank you for your article.
There is a possibility to do the same function using multiples CardView instead of listView?
yes you can replace list view with card view
Hi,
Can you please also explain how can we Upgrade/Downgrade the Subscription?
I have not check that yet
I have successfully implemented the code and submitted the app-bundle to the google play console.
I have registered the tester’s email address. but when I tested, it turned out that my tester account did not have a test card payment option. do you know why?
* In a case of closed alpha/beta testing, make sure you have added your test account to selected testers group, you can do this on the page of management your alpha/beta version.
* In a case of closed alpha/beta testing, make sure your testing account have accepted participation in testing of this application via special invite link
thanks
Hi ,
Thank you for your article . I have a problem and I can’t fix it . It show “Item not Found” after clicked on “SUBSCRIBE” button. I’m using a old subscription from Play Console , who work great in other application . Can you tell me how I can fix this problem ?
Thanks
Below are different ways of fixing it.
Please try to clear data and cache of your playstore app on your phone to see if fixed
If not then wait for one day for item to be available Click Here For info
Furthermore make sure app is uploaded on playstore in any track with billing permission added and product id in your Play console is same as product id used in your app project.
Thanks