How to make In App Purchase Subscription in Android Using Google Play Billing Library

Share Post
  •  
  •  
  •  
  • 1
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    1
    Share

Please Subscribe Youtube| Like Facebook | Follow Twitter

NOTE:

If you need personal technical assistance for integrating Google Play Billing Library with latest version than you can book order with me via Fiver here.

Introduction

In this article we will learn how to integrate and make Single In App Subscription (monthly) using google play billing library.

Tips

ArticleJavaKotlin
For Single Non-consumable one-time productlinklink
For Multiple Non-consumable one-time productlinklink
For Single Consumable one-time productlinklink
For Multiple Consumable one-time productlinklink
For Single In App Subscriptionslinklink
For Multiple In App Subscriptionslinklink

Our Example

In our example we have two textview’s which show premium content and subscription status respectively and a button which allow user to subscribe (monthly) to see premium content. When user is not subscribed then premium content textview will be hidden and subscription status will be “Not Subscribed”. After Subscription premium content textview will be visible and subscription status will be “Subscribed” and subscribe button will be hidden.

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 app launch we will update subscription status in preference and premium content will be locked on next app launch unless user subscribe it again.

Requirements:

  1. Android Project/App to which In App Subscription Item to be added
  2. Google play console account

Note: In order to create in app subscription items in Google Console or perform real test of in app subscription item in your app, you first need to Integrate Google Play Library and add billing permission in your project and 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 item “sub_example” in Google play console

5 Code in app subscription 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 item “sub_example” in Google play console

After releasing app go to Store presence -> In-app products -> Subscriptions

Click on create subscriptions

Enter product id, name and description

Enter billing period, for demo purpose we are using monthly subscription.

Enter price according to your requirement, we have entered rs 500 PKR

Fill Subscription option according to your requirements, to keep things simple we have disabled free trial, introductory price and grace period. After filling click on save and then activate it.

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 subscription flow logic.

First make sure you have added Google Play Billing Library dependency and billing permission, which was described in step 1.

Then Add two textviews Premium Content, Subscription Status and subscribe button 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"
    tools:context="com.programtown.example.MainActivity"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/premium_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="4"
        android:text="Premium Content"
        android:gravity="center"
        android:visibility="gone"
          />
    <TextView
        android:id="@+id/subscription_status"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="4"
        android:gravity="center"
        />
    <Button
        android:id="@+id/subscribe"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="subscribe"
        android:visibility="gone"
        android:onClick="subscribe"
        />

</LinearLayout>

In order to make In-App Subscription 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
}

}

  On subscription button click we will initiate purchase flow.

public void subscribe(View view) {
        //check if service is already connected
        if (billingClient.isReady()) {
            initiatePurchase();
        }
        //else reconnect service
        else{
            billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
            billingClient.startConnection(new BillingClientStateListener() {
                @Override
                public void onBillingSetupFinished(BillingResult billingResult) {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        initiatePurchase();
                    } else {
                        Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
                    }
                }
                @Override
                public void onBillingServiceDisconnected() {
                    Toast.makeText(getApplicationContext(),"Service Disconnected ",Toast.LENGTH_SHORT).show();
                }
            });
        }
    }

In initiatePurchase() method we have included subscription product id “sub_example”, which was added in Google Play Console inside in app products (subscriptions), 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() {
        List<String> skuList = new ArrayList<>();
        skuList.add(ITEM_SKU_SUBSCRIBE);
        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(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 subscription item "sub_example" in google play console
                                    Toast.makeText(getApplicationContext(), "Item 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.

public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
        //if item subscribed
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            handlePurchases(purchases);
        }
        //if item already subscribed 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 canceled
        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 purchase, acknowledgement is necessary because failure to properly acknowledge purchase will result in purchase being refunded. After the acknowledgement we will save subscription value in preference and restart activity to make necessary changes e.g. show premium content, change subscription status and hide subscription button.

This method also toast user to complete transaction if purchase status is pending. In case of unspecified purchase we will change purchase status to “not subscribed”, hide premium content and store updated value in preference.

void handlePurchases(List<Purchase>  purchases) {
        for(Purchase purchase:purchases) {
            //if item is purchased
            if (ITEM_SKU_SUBSCRIBE.equals(purchase.getSku()) && 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();
                    return;
                }
                // else purchase is valid
                //if item is purchased and not acknowledged
                if (!purchase.isAcknowledged()) {
                    AcknowledgePurchaseParams acknowledgePurchaseParams =
                            AcknowledgePurchaseParams.newBuilder()
                                    .setPurchaseToken(purchase.getPurchaseToken())
                                    .build();
                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase);
                }
                //else item is purchased and also acknowledged
                else {
                    // Grant entitlement to the user on item purchase
                    // restart activity
                    if(!getSubscribeValueFromPref()){
                        saveSubscribeValueToPref(true);
                        Toast.makeText(getApplicationContext(), "Item Purchased", Toast.LENGTH_SHORT).show();
                        this.recreate();
                    }
                }
            }
            //if purchase is pending
            else if( ITEM_SKU_SUBSCRIBE.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
            {
                Toast.makeText(getApplicationContext(),
                        "Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
            }
            //if purchase is unknown mark false
            else if(ITEM_SKU_SUBSCRIBE.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
            {
                saveSubscribeValueToPref(false);
                premiumContent.setVisibility(View.GONE);
                subscribe.setVisibility(View.VISIBLE);
                subscriptionStatus.setText("Subscription Status : Not Subscribed");
                Toast.makeText(getApplicationContext(), "Purchase Status Unknown", Toast.LENGTH_SHORT).show();
            }
        }
    }

AcknowledgePurchaseResponseListener ackPurchase = new AcknowledgePurchaseResponseListener() {
	@Override
	public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
		if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
			//if purchase is acknowledged
			// Grant entitlement to the user. and restart activity
			saveSubscribeValueToPref(true);
			MainActivity.this.recreate();
		}
	}
};

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 item 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.

// Establish connection to billing client
//check subscription status from google play store cache
//to check if item is already Subscribed or subscription is not renewed and cancelled
billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
billingClient.startConnection(new BillingClientStateListener() {
	@Override
	public void onBillingSetupFinished(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);
			}
			//if no item in purchase list means subscription is not subscribed
			//Or subscription is cancelled and not renewed for next month
			// so update pref in both cases
			// so next time on app launch our premium content will be locked again
			else{
				saveSubscribeValueToPref(false);
			}
		}
	}

	@Override
	public void onBillingServiceDisconnected() {
		Toast.makeText(getApplicationContext(),"Service Disconnected",Toast.LENGTH_SHORT).show();
	}
});

//item subscribed
if(getSubscribeValueFromPref()){
	subscribe.setVisibility(View.GONE);
	premiumContent.setVisibility(View.VISIBLE);
	subscriptionStatus.setText("Subscription Status : Subscribed");
}
//item not subscribed
else{
	premiumContent.setVisibility(View.GONE);
	subscribe.setVisibility(View.VISIBLE);
	subscriptionStatus.setText("Subscription Status : Not Subscribed");
}

Whole Code

App level build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion "29.0.3"
    defaultConfig {
        applicationId "com.programtown.example"
        minSdkVersion 17
        targetSdkVersion 28
        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.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

    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="com.programtown.example.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"
    tools:context="com.programtown.example.MainActivity"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/premium_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="4"
        android:text="Premium Content"
        android:gravity="center"
        android:visibility="gone"
          />
    <TextView
        android:id="@+id/subscription_status"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="4"
        android:gravity="center"
        />
    <Button
        android:id="@+id/subscribe"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="subscribe"
        android:visibility="gone"
        android:onClick="subscribe"
        />

</LinearLayout>

MainActivity.java

package com.programtown.example;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
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";
    public static final String SUBSCRIBE_KEY= "subscribe";
    public static final String ITEM_SKU_SUBSCRIBE= "sub_example";

    TextView premiumContent,subscriptionStatus;
    Button subscribe;

    private BillingClient billingClient;

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

        premiumContent = (TextView) findViewById(R.id.premium_content);
        subscriptionStatus=(TextView) findViewById(R.id.subscription_status);
        subscribe=(Button) findViewById(R.id.subscribe);

        // Establish connection to billing client
        //check subscription status from google play store cache
        //to check if item is already Subscribed or subscription is not renewed and cancelled
        billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(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);
                    }
                    //if no item in purchase list means subscription is not subscribed
                    //Or subscription is cancelled and not renewed for next month
                    // so update pref in both cases
                    // so next time on app launch our premium content will be locked again
                    else{
                        saveSubscribeValueToPref(false);
                    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                Toast.makeText(getApplicationContext(),"Service Disconnected",Toast.LENGTH_SHORT).show();
            }
        });

        //item subscribed
        if(getSubscribeValueFromPref()){
            subscribe.setVisibility(View.GONE);
            premiumContent.setVisibility(View.VISIBLE);
            subscriptionStatus.setText("Subscription Status : Subscribed");
        }
        //item not subscribed
        else{
            premiumContent.setVisibility(View.GONE);
            subscribe.setVisibility(View.VISIBLE);
            subscriptionStatus.setText("Subscription Status : Not Subscribed");
        }
    }

    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 getSubscribeValueFromPref(){
        return getPreferenceObject().getBoolean( SUBSCRIBE_KEY,false);
    }
    private void saveSubscribeValueToPref(boolean value){
        getPreferenceEditObject().putBoolean(SUBSCRIBE_KEY,value).commit();
    }

    //initiate purchase on button click
    public void subscribe(View view) {
        //check if service is already connected
        if (billingClient.isReady()) {
            initiatePurchase();
        }
        //else reconnect service
        else{
            billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
            billingClient.startConnection(new BillingClientStateListener() {
                @Override
                public void onBillingSetupFinished(BillingResult billingResult) {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        initiatePurchase();
                    } else {
                        Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
                    }
                }
                @Override
                public void onBillingServiceDisconnected() {
                    Toast.makeText(getApplicationContext(),"Service Disconnected ",Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
    private void initiatePurchase() {
        List<String> skuList = new ArrayList<>();
        skuList.add(ITEM_SKU_SUBSCRIBE);
        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(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 subscription item "sub_example" in google play console
                                    Toast.makeText(getApplicationContext(), "Item 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 subscribed
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            handlePurchases(purchases);
        }
        //if item already subscribed 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 canceled
        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) {
            //if item is purchased
            if (ITEM_SKU_SUBSCRIBE.equals(purchase.getSku()) && 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();
                    return;
                }
                // else purchase is valid
                //if item is purchased and not acknowledged
                if (!purchase.isAcknowledged()) {
                    AcknowledgePurchaseParams acknowledgePurchaseParams =
                            AcknowledgePurchaseParams.newBuilder()
                                    .setPurchaseToken(purchase.getPurchaseToken())
                                    .build();
                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase);
                }
                //else item is purchased and also acknowledged
                else {
                    // Grant entitlement to the user on item purchase
                    // restart activity
                    if(!getSubscribeValueFromPref()){
                        saveSubscribeValueToPref(true);
                        Toast.makeText(getApplicationContext(), "Item Purchased", Toast.LENGTH_SHORT).show();
                        this.recreate();
                    }
                }
            }
            //if purchase is pending
            else if( ITEM_SKU_SUBSCRIBE.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
            {
                Toast.makeText(getApplicationContext(),
                        "Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
            }
            //if purchase is unknown mark false
            else if(ITEM_SKU_SUBSCRIBE.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
            {
                saveSubscribeValueToPref(false);
                premiumContent.setVisibility(View.GONE);
                subscribe.setVisibility(View.VISIBLE);
                subscriptionStatus.setText("Subscription Status : Not Subscribed");
                Toast.makeText(getApplicationContext(), "Purchase Status Unknown", Toast.LENGTH_SHORT).show();
            }
        }
    }
    AcknowledgePurchaseResponseListener ackPurchase = new AcknowledgePurchaseResponseListener() {
        @Override
        public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
            if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
                //if purchase is acknowledged
                // Grant entitlement to the user. and restart activity
                saveSubscribeValueToPref(true);
                MainActivity.this.recreate();
            }
        }
    };

    /**
     * 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 = "Enter 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

/*
 * right (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 subscription 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.

Now After Subscribing, test case of subscription renew is that it will renew 6 times for period of 5 min (for testing it is 5 min in real subscription it will be one month), then it will be cancelled after 6 time renewal. See here for info

When subscription is not renewed after expiry then on app launch preference value will be updated and then on next app launch premium content will be locked again.

Conclusion

So in this article we have learned how to integrate and make in app purchase of monthly subscription. If you liked the article then please share this page and article. Thanks.

NOTE:

If you need personal technical assistance for integrating Google Play Billing Library with latest version than you can book order with me via Fiver here.

References:

https://support.google.com/googleplay/answer/7018481?co=GENIE.Platform%3DAndroid&hl=en

https://developer.android.com/google/play/billing/billing_testing?authuser=1#testing-subscriptions

https://github.com/android/play-billing-samples/blob/551a178e52baf60cc6e1f9cb6f40767b8453655a/TrivialDrive/app/src/main/java/com/example/android/trivialdrivesample/util/Security.java

Please Subscribe Youtube| Like Facebook | Follow Twitter


Share Post
  •  
  •  
  •  
  • 1
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    1
    Share

29 Replies to “How to make In App Purchase Subscription in Android Using Google Play Billing Library”

  1. Hi,

    Thanks for this tutorial it was very helpful, but i have a question that made me confused.

    For my understandings that we have to upload the app first to play console then create subscribe to be able to get keys and sub names and so on.

    did you mean that we have to submit another version of the app once we created subscribe that have all the values needed?

    I hope you got my point.

    Thanks in Advance

  2. Hi, thanks for this article.
    Can you pls show me how to create a class for it and call the method in other activities. Thanks

  3. thank you very much bro u saved my day

    ..I have a stupid question.. how i make sure the user is a subscriber or not to decide if I will show him ads or not

  4. Thanks, I subscribed your youtube channel. And I have one request.
    For example , in 2 Activity (MainActivity, SettingActivity),
    When touch the subscribe button in SettingActivity, how to change logic in MainActivity?
    Can you write the Tip post? I’m currently learning android. It’s difficult to me.
    please help!

    1. I think you need to create BillingClient singleton object and create it inside application activity and whenever which activity needed purchase logic then just attach BillingClient Object to it.
      thanks
      For more info

      Please check this
      i.e
      It looks like this can be done with architecture components. I.e. in your application’s OnCreate, call:
      ProcessLifecycleOwner.get().lifecycle.addObserver(billingClient)
      And just inject the billingClient into the activities that need it.

      link:
      https://stackoverflow.com/questions/53111773/one-instance-of-billingclient-throughout-app

      And this
      https://github.com/android/play-billing-samples/tree/master/ClassyTaxiJava

  5. Thank you so much.
    I followed this article exactly, but I got this error.

    “Error invalid SKU details”

    Payment information does not appear.

    1. skuList.add(ITEM_SKU_SUBSCRIBE);
      SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
      params.setSkusList(skuList).setType(SUBS);

      Make sure product id in your Play console is same as ITEM_SKU_SUBSCRIBE used in your app project.
      Furthermore make sure your app is uploaded on playstore in any track with billing permission.
      Kindly recheck your code.

      And please subscribe our youtube channel
      Thanks

  6. I’m doing something wrong. After checking the subscription, the application does not start further. First I pasted your code into MainActivity and then the application code comes. I don’t know how to connect them correctly. I did like this…

    @Override
    protected void onDestroy() {
    super.onDestroy();
    if (billingClient != null) {
    billingClient.endConnection();
    }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    setContentView(R.layout.activity_main);
    view = findViewById(android.R.id.content);

    Need advice

    1. what are your doing in onRestoreInstanceState method?
      we are setting layout in oncreate method.
      @Override
      protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      }

  7. Hello. Thank you for the article. I’m new to this business and I don’t understand how to insert your codes into an already finished application. I already have MainActivity and activity_main. I need to create separate files for your codes or insert them into existing ones.

  8. Hello, there’s one thing I don’t get:
    /**
    * Verifies that the purchase was signed correctly for this developer’s public key.
    * 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.
    *
    */
    I understand what you mean. But if some hackers are able to decompile/rebuild the app, they can also just remove all “subscription” checks everywhere. After all it’s a boolean in sharedprefs… Or am i missing something? I have a VPS btw, but don’t really understand what I should implement over there…

  9. Hi bro!, Thank you so much for a great article!

    but I faced the problem it always show “Item not Found” after clicked on “SUBSCRIBE” button. I’m already created “sub_example” subscription in Play Console

    What the problem is? How can I solve it bro?

    1. 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.
      Thanks

    1. Hi

      You can use
      ‘com.android.billingclient:billing:3.0.0’
      library with this article’s example code. It is compatible. Let me know if any issue arise.

      Thanks

  10. Thank you very much.
    And I have a question. If I have a several subscriptions and of them was cancelled or not renewed, so how can I save this to Preferences. I have two preferences keys.

    Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(SUBS);
    List queryPurchases = queryPurchase.getPurchasesList();
    if(queryPurchases!=null && queryPurchases.size()>0){
    handlePurchases(queryPurchases);
    }
    //if no item in purchase list means subscription is not subscribed
    //Or subscription is cancelled and not renewed for next month
    // so update pref in both cases
    // so next time on app launch our premium content will be locked again
    else{
    saveSubscribeValueToPref(false);
    }

    1. Then you have to check for each subscription. For example I Have two subscription
      i.e sub1,sub2
      
      
      Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(SUBS);
      List queryPurchases = queryPurchase.getPurchasesList();
      if(queryPurchases!=null && queryPurchases.size()>0){
      	handlePurchases(queryPurchases);
      }
      //if no item in purchase list means subscription is not subscribed
      //Or subscription is cancelled and not renewed for next month
      // so update pref in both cases
      // so next time on app launch our premium content will be locked again
      else{
      	saveSubscribe1ValueToPref(false);
      	saveSubscribe2ValueToPref(false);
      }
      
      void handlePurchases(List  purchases) {
      	for(Purchase purchase:purchases) {
      	
      		if(ITEM_SKU_SUBSCRIBE_1.equals(purchase.getSku())){
      			//if item1 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 purchase
      				}
      				// else purchase is valid
      				//if item1 is purchased and not acknowledged
      				if (!purchase.isAcknowledged()) {
      					AcknowledgePurchaseParams acknowledgePurchaseParams =
      							AcknowledgePurchaseParams.newBuilder()
      									.setPurchaseToken(purchase.getPurchaseToken())
      									.build();
      					billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase1);
      				}
      				//else item1 is purchased and also acknowledged
      				else {
      					// Grant entitlement to the user on item purchase
      					if(!getSubscribeValue1FromPref()){
      						saveSubscribeValue1ToPref(true);
      						Toast.makeText(getApplicationContext(), "Item Purchased. Please Restart App", Toast.LENGTH_SHORT).show();
      					}
      				}
      			}
      			//if purchase is pending
      			else if(purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
      			{
      				Toast.makeText(getApplicationContext(),
      						"Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
      			}
      			//if purchase is unknown mark false
      			else if( purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
      			{
      				saveSubscribe1ValueToPref(false);
      				premiumContent1.setVisibility(View.GONE);
      				subscribe1.setVisibility(View.VISIBLE);
      				subscriptionStatus1.setText("Subscription 1 Status : Not Subscribed");
      				Toast.makeText(getApplicationContext(), "Purchase Status Unknown", Toast.LENGTH_SHORT).show();
      			}
      		}
      
      		else if(ITEM_SKU_SUBSCRIBE_2.equals(purchase.getSku())){
      			//if item2 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 purchase
      				}
      				// else purchase is valid
      				//if item2 is purchased and not acknowledged
      				if (!purchase.isAcknowledged()) {
      					AcknowledgePurchaseParams acknowledgePurchaseParams =
      							AcknowledgePurchaseParams.newBuilder()
      									.setPurchaseToken(purchase.getPurchaseToken())
      									.build();
      					billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase2);
      				}
      				//else item2 is purchased and also acknowledged
      				else {
      					// Grant entitlement to the user on item purchase
      					if(!getSubscribeValue2FromPref()){
      						saveSubscribeValue2ToPref(true);
      						Toast.makeText(getApplicationContext(), "Item Purchased. Please Restart App", Toast.LENGTH_SHORT).show();
      					}
      				}
      			}
      			//if purchase is pending
      			else if(purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
      			{
      				Toast.makeText(getApplicationContext(),
      						"Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
      			}
      			//if purchase is unknown mark false
      			else if( purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
      			{
      				saveSubscribe2ValueToPref(false);
      				premiumContent2.setVisibility(View.GONE);
      				subscribe2.setVisibility(View.VISIBLE);
      				subscriptionStatus2.setText("Subscription 1 Status : Not Subscribed");
      				Toast.makeText(getApplicationContext(), "Purchase Status Unknown", Toast.LENGTH_SHORT).show();
      			}
      		}	
      
      	}
      }
      
      
      AcknowledgePurchaseResponseListener ackPurchase1 = new AcknowledgePurchaseResponseListener() {
      	@Override
      	public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
      		if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
      			//if purchase is acknowledged
      			// Grant entitlement to the user
      			saveSubscribeValue1ToPref(true);
      			Toast.makeText(getApplicationContext(), "Item Purchased. Please Restart App", Toast.LENGTH_SHORT).show();
      		}
      	}
      };
      
      AcknowledgePurchaseResponseListener ackPurchase2 = new AcknowledgePurchaseResponseListener() {
      @Override
      public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
      	if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
      		//if purchase is acknowledged
      		// Grant entitlement to the user. and restart activity
      		saveSubscribeValue2ToPref(true);
      		Toast.makeText(getApplicationContext(), "Item Purchased. Please Restart App", Toast.LENGTH_SHORT).show();
      	}
      }
      };	
      
      1. Yes, you are right. But if one of subscription is purchased, the code will not be work without the example code below:

        Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(BillingClient.SkyType.SUBS);
        List queryPurchases = queryPurchase.getPurchasesList();
        if(queryPurchases!=null && queryPurchases.size()>0){
        handlePurchases(queryPurchases);
        if(queryPurchases.size() == 1){
        for(Purchase purchase : queryPurchases) {
        String sku = purchase.getSku();
        switch (sku) {
        case ITEM_SKU_SUBSCRIBE_1:
        saveSubscribeValue2ToPref(false);
        break;
        case ITEM_SKU_SUBSCRIBE_2:
        saveSubscribeValue1ToPref(false);
        break;
        }
        }
        }
        }
        //if no item in purchase list means subscription is not subscribed
        //Or subscription is cancelled and not renewed for next month
        // so update pref in both cases
        // so next time on app launch our premium content will be locked again
        else{
        saveSubscribe1ValueToPref(false);
        saveSubscribe2ValueToPref(false);
        }

        I know it is not a very correct approach, but it works for me.

Leave a Reply

Your email address will not be published. Required fields are marked *