How to make In App Purchase 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 Single In App Subscription (monthly) 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 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:
- Android Project/App to which In App Subscription Item to be added
- 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:
Need Support?
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
Please Subscribe Youtube| Like Facebook | Follow Twitter
I have a problem with the following line of code:
private boolean verifyValidSignature(String signedData, String signature) {
try {
// To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
String base64Key = “”;
return Security.verifyPurchase(base64Key, signedData, signature);
} catch(IOException e) {
return false;
}
}
Error:
return Security.verifyPurchase(base64Key, signedData, signature);
^
symbol: Security variable
location: class VipActivity
please add Security class and also add base64Key from your play console account.
You can also book order for personal support and implementation with latest library via Fiver.
https://www.fiverr.com/share/NEW6eN
Thanks
Hi, thank you so much this article really helped me. I have implemented the codes.
But, I still have a problem with the listing of the subscription. I have added 4 SKU (1 month,3 month, 6 month and 1 year), but it only display the first one, also the ‘1 month’. What could be wrong here?
Many thanks before.
Cheers
please reconfirm the matching of product id on console and product id in code.
You can book order for personal support and implementation with latest library via Fiver.
https://www.fiverr.com/share/NEW6eN
Thanks
I integrated the codes into my app. I have successfully completed the subscription process. But it asks me to verify on google play. I want to verify the subscription from my google account but it is not approved. premium membership is not active. where could i be going wrong? can u help me ?
it is not technical issue may be issue with google side
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
no just you need to upload signed apk with billing permission added to console only once. thanks
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
just implement interface where you want in other activities
Thank you now implementing on android tv
thanks
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
we are saving Boolean flag value in preference through that we can check if user is subscriber or not.
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!
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
Thank you so much.
I followed this article exactly, but I got this error.
“Error invalid SKU details”
Payment information does not appear.
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
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
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);
}
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.
dear you need to insert into existing ones.
Thanks a lot.
You have helped me in setting up a subscription in my app.
thank you please share this site and subscribe our Youtube channel.
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…
In this article we have implemented only client side verification for purchase which is less secure because it can be reverse engineered.
But you can obfuscate your apk. It will be then hard to crack.
thanks
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?
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
Hi ,
How did you fix the problem , because I have some issue like you .
Thank you .
Hi,
Do you have an example for the new mandatory:
‘com.android.billingclient:billing:3.0.0’
Thanks
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
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);
}
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.
yes thanks for pointing out. Your approach is good for two subscription items. In case of more than two, we have to optimize code.