HOW TO MAKE IN APP PURCHASE SUBSCRIPTION IN ANDROID/Kotlin 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.

class MainActivity : AppCompatActivity(), PurchasesUpdatedListener {
    private var billingClient: BillingClient? = null
    protected fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(layout.activity_main)
        billingClient = BillingClient.newBuilder(this)
                .enablePendingPurchases().setListener(this).build()
    }

    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
        //we will code subscribtion logic later

    }
}

  On subscription button click we will initiate purchase flow.

//initiate purchase on button click
fun subscribe(view: View?) {
	//check if service is already connected
	if (billingClient!!.isReady) {
		initiatePurchase()
	} else {
		billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build()
		billingClient!!.startConnection(object : BillingClientStateListener {
			override fun onBillingSetupFinished(billingResult: BillingResult) {
				if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
					initiatePurchase()
				} else {
					Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
				}
			}

			override fun onBillingServiceDisconnected() {
				Toast.makeText(applicationContext, "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 fun initiatePurchase() {
	val skuList: MutableList<String> = ArrayList()
	skuList.add(ITEM_SKU_SUBSCRIBE)
	val params = SkuDetailsParams.newBuilder()
	params.setSkusList(skuList).setType(SkuType.SUBS)
	val billingResult = billingClient!!.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS)
	if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
		billingClient!!.querySkuDetailsAsync(params.build()
		) { billingResult, skuDetailsList ->
			if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
				if (skuDetailsList != null && skuDetailsList.size > 0) {
					val flowParams = BillingFlowParams.newBuilder()
							.setSkuDetails(skuDetailsList[0])
							.build()
					billingClient!!.launchBillingFlow([email protected], flowParams)
				} else {
					//try to add subscription item "sub_example" in google play console
					Toast.makeText(applicationContext, "Item not Found", Toast.LENGTH_SHORT).show()
				}
			} else {
				Toast.makeText(applicationContext,
						" Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
			}
		}
	} else {
		Toast.makeText(applicationContext,
				"Sorry Subscription not Supported. Please Update Play Store", Toast.LENGTH_SHORT).show()
	}
}

In onPurchasesUpdated() method we will check purchase result and handle it accordingly.

override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
	//if item subscribed
	if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
		handlePurchases(purchases)
	} 
	else if (billingResult.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
		val queryAlreadyPurchasesResult = billingClient!!.queryPurchases(SkuType.SUBS)
		val alreadyPurchases = queryAlreadyPurchasesResult.purchasesList
		alreadyPurchases?.let { handlePurchases(it) }
	} 
	else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
		Toast.makeText(applicationContext, "Purchase Canceled", Toast.LENGTH_SHORT).show()
	} 
	else {
		Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, 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.

fun handlePurchases(purchases: List<Purchase>) {
	for (purchase in purchases) {
		//if item is purchased
		if (ITEM_SKU_SUBSCRIBE == purchase.sku && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
			if (!verifyValidSignature(purchase.originalJson, purchase.signature)) {
				// Invalid purchase
				// show error to user
				Toast.makeText(applicationContext, "Error : invalid Purchase", Toast.LENGTH_SHORT).show()
				return
			}
			// else purchase is valid
			//if item is purchased and not acknowledged
			if (!purchase.isAcknowledged) {
				val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
						.setPurchaseToken(purchase.purchaseToken)
						.build()
				billingClient!!.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase)
			} else {
				// Grant entitlement to the user on item purchase
				// restart activity
				if (!subscribeValueFromPref) {
					saveSubscribeValueToPref(true)
					Toast.makeText(applicationContext, "Item Purchased", Toast.LENGTH_SHORT).show()
					recreate()
				}
			}
		} else if (ITEM_SKU_SUBSCRIBE == purchase.sku && purchase.purchaseState == Purchase.PurchaseState.PENDING) {
			Toast.makeText(applicationContext,
					"Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show()
		} else if (ITEM_SKU_SUBSCRIBE == purchase.sku && purchase.purchaseState == Purchase.PurchaseState.UNSPECIFIED_STATE) {
			saveSubscribeValueToPref(false)
			premiumContent!!.visibility = View.GONE
			subscribe!!.visibility = View.VISIBLE
			subscriptionStatus!!.text = "Subscription Status : Not Subscribed"
			Toast.makeText(applicationContext, "Purchase Status Unknown", Toast.LENGTH_SHORT).show()
		}
	}
}

var ackPurchase = AcknowledgePurchaseResponseListener { billingResult ->
	if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
		//if purchase is acknowledged
		// Grant entitlement to the user. and restart activity
		saveSubscribeValueToPref(true)
		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.
     *
     * 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.
     *
     */
    private fun verifyValidSignature(signedData: String, signature: String): Boolean {
        return try {
            // To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
            val base64Key = "Enter Your Key Here"
            Security.verifyPurchase(base64Key, signedData, signature)
        } catch (e: IOException) {
            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(object : BillingClientStateListener {
	override fun onBillingSetupFinished(billingResult: BillingResult) {
		if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
			val queryPurchase = billingClient!!.queryPurchases(SkuType.SUBS)
			val queryPurchases = queryPurchase.purchasesList
			if (queryPurchases != null && queryPurchases.size > 0) {
				handlePurchases(queryPurchases)
			} else {
				saveSubscribeValueToPref(false)
			}
		}
	}

	override fun onBillingServiceDisconnected() {
		Toast.makeText(applicationContext, "Service Disconnected", Toast.LENGTH_SHORT).show()
	}
})

//item subscribed
if (subscribeValueFromPref) {
	subscribe!!.visibility = View.GONE
	premiumContent!!.visibility = View.VISIBLE
	subscriptionStatus!!.text = "Subscription Status : Subscribed"
} else {
	premiumContent!!.visibility = View.GONE
	subscribe!!.visibility = View.VISIBLE
	subscriptionStatus!!.text = "Subscription Status : Not Subscribed"
}

Whole Code

Project level build.gradle

buildscript {
    ext.kotlin_version = '1.4.0-rc'
    repositories {
        jcenter()
        google()
        maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

App level build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'

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'
    implementation "androidx.core:core-ktx:+"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

}
repositories {
    maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
    mavenCentral()
}

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

package com.programtown.example

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 androidx.appcompat.app.AppCompatActivity
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.SkuType
import java.io.IOException
import java.util.*

class MainActivity : AppCompatActivity(), PurchasesUpdatedListener {

    var premiumContent: TextView? = null
    var subscriptionStatus: TextView? = null
    var subscribe: Button? = null
    private var billingClient: BillingClient? = null

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        premiumContent = findViewById<View>(R.id.premium_content) as TextView
        subscriptionStatus = findViewById<View>(R.id.subscription_status) as TextView
        subscribe = findViewById<View>(R.id.subscribe) as Button

        // 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(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    val queryPurchase = billingClient!!.queryPurchases(SkuType.SUBS)
                    val queryPurchases = queryPurchase.purchasesList
                    if (queryPurchases != null && queryPurchases.size > 0) {
                        handlePurchases(queryPurchases)
                    } else {
                        saveSubscribeValueToPref(false)
                    }
                }
            }

            override fun onBillingServiceDisconnected() {
                Toast.makeText(applicationContext, "Service Disconnected", Toast.LENGTH_SHORT).show()
            }
        })

        //item subscribed
        if (subscribeValueFromPref) {
            subscribe!!.visibility = View.GONE
            premiumContent!!.visibility = View.VISIBLE
            subscriptionStatus!!.text = "Subscription Status : Subscribed"
        } else {
            premiumContent!!.visibility = View.GONE
            subscribe!!.visibility = View.VISIBLE
            subscriptionStatus!!.text = "Subscription Status : Not Subscribed"
        }
    }

    private val preferenceObject: SharedPreferences
        get() = applicationContext.getSharedPreferences(PREF_FILE, 0)

    private val preferenceEditObject: SharedPreferences.Editor
        get() {
            val pref = applicationContext.getSharedPreferences(PREF_FILE, 0)
            return pref.edit()
        }

    private val subscribeValueFromPref: Boolean
        get() = preferenceObject.getBoolean(SUBSCRIBE_KEY, false)

    private fun saveSubscribeValueToPref(value: Boolean) {
        preferenceEditObject.putBoolean(SUBSCRIBE_KEY, value).commit()
    }

    //initiate purchase on button click
    fun subscribe(view: View?) {
        //check if service is already connected
        if (billingClient!!.isReady) {
            initiatePurchase()
        } else {
            billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build()
            billingClient!!.startConnection(object : BillingClientStateListener {
                override fun onBillingSetupFinished(billingResult: BillingResult) {
                    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                        initiatePurchase()
                    } else {
                        Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
                    }
                }

                override fun onBillingServiceDisconnected() {
                    Toast.makeText(applicationContext, "Service Disconnected ", Toast.LENGTH_SHORT).show()
                }
            })
        }
    }

    private fun initiatePurchase() {
        val skuList: MutableList<String> = ArrayList()
        skuList.add(ITEM_SKU_SUBSCRIBE)
        val params = SkuDetailsParams.newBuilder()
        params.setSkusList(skuList).setType(SkuType.SUBS)
        val billingResult = billingClient!!.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS)
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            billingClient!!.querySkuDetailsAsync(params.build()
            ) { billingResult, skuDetailsList ->
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    if (skuDetailsList != null && skuDetailsList.size > 0) {
                        val flowParams = BillingFlowParams.newBuilder()
                                .setSkuDetails(skuDetailsList[0])
                                .build()
                        billingClient!!.launchBillingFlow([email protected], flowParams)
                    } else {
                        //try to add subscription item "sub_example" in google play console
                        Toast.makeText(applicationContext, "Item not Found", Toast.LENGTH_SHORT).show()
                    }
                } else {
                    Toast.makeText(applicationContext,
                            " Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
                }
            }
        } else {
            Toast.makeText(applicationContext,
                    "Sorry Subscription not Supported. Please Update Play Store", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
        //if item subscribed
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            handlePurchases(purchases)
        }
        else if (billingResult.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
            val queryAlreadyPurchasesResult = billingClient!!.queryPurchases(SkuType.SUBS)
            val alreadyPurchases = queryAlreadyPurchasesResult.purchasesList
            alreadyPurchases?.let { handlePurchases(it) }
        }
        else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
            Toast.makeText(applicationContext, "Purchase Canceled", Toast.LENGTH_SHORT).show()
        }
        else {
            Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
        }
    }

    fun handlePurchases(purchases: List<Purchase>) {
        for (purchase in purchases) {
            //if item is purchased
            if (ITEM_SKU_SUBSCRIBE == purchase.sku && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                if (!verifyValidSignature(purchase.originalJson, purchase.signature)) {
                    // Invalid purchase
                    // show error to user
                    Toast.makeText(applicationContext, "Error : invalid Purchase", Toast.LENGTH_SHORT).show()
                    return
                }
                // else purchase is valid
                //if item is purchased and not acknowledged
                if (!purchase.isAcknowledged) {
                    val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                            .setPurchaseToken(purchase.purchaseToken)
                            .build()
                    billingClient!!.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase)
                } else {
                    // Grant entitlement to the user on item purchase
                    // restart activity
                    if (!subscribeValueFromPref) {
                        saveSubscribeValueToPref(true)
                        Toast.makeText(applicationContext, "Item Purchased", Toast.LENGTH_SHORT).show()
                        recreate()
                    }
                }
            } else if (ITEM_SKU_SUBSCRIBE == purchase.sku && purchase.purchaseState == Purchase.PurchaseState.PENDING) {
                Toast.makeText(applicationContext,
                        "Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show()
            } else if (ITEM_SKU_SUBSCRIBE == purchase.sku && purchase.purchaseState == Purchase.PurchaseState.UNSPECIFIED_STATE) {
                saveSubscribeValueToPref(false)
                premiumContent!!.visibility = View.GONE
                subscribe!!.visibility = View.VISIBLE
                subscriptionStatus!!.text = "Subscription Status : Not Subscribed"
                Toast.makeText(applicationContext, "Purchase Status Unknown", Toast.LENGTH_SHORT).show()
            }
        }
    }

    var ackPurchase = AcknowledgePurchaseResponseListener { billingResult ->
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            //if purchase is acknowledged
            // Grant entitlement to the user. and restart activity
            saveSubscribeValueToPref(true)
            recreate()
        }
    }

    /**
     * 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.
     *
     */
    private fun verifyValidSignature(signedData: String, signature: String): Boolean {
        return try {
            // To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
            val base64Key = "Enter Your Key Here"
            Security.verifyPurchase(base64Key, signedData, signature)
        } catch (e: IOException) {
            false
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (billingClient != null) {
            billingClient!!.endConnection()
        }
    }

    companion object {
        const val PREF_FILE = "MyPref"
        const val SUBSCRIBE_KEY = "subscribe"
        const val ITEM_SKU_SUBSCRIBE = "sub_example"
    }
}

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

7 Replies to “HOW TO MAKE IN APP PURCHASE SUBSCRIPTION IN ANDROID/Kotlin USING GOOGLE PLAY BILLING LIBRARY”

  1. Help,! I am getting “Error: Invalid Purchase” after successfully subscribed (green tick). I am using latest library 3.0.3

  2. How to add 3 subscription items in this code you put example of only one?
    Also can we fetch prices from google ?

    1. For multiple item you can follow this comment.
      Click Here
      
      You will get idea for how to implement it.
      It is in java so convert it in kotlin and just change 
      
      params.setSkusList(skuList).setType(SkuType.INAPP);
      To
      params.setSkusList(skuList).setType(SkuType.SUBS);
      For price you can fetch from SkuDetails object's getPrice() or 	getOriginalPrice()
      method.
      
      if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
      	billingClient!!.querySkuDetailsAsync(params.build()
      	) { billingResult, skuDetailsList ->
      		if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
      			if (skuDetailsList != null && skuDetailsList.size > 0) {
      				
      				//here price of that particular item will be fetched
      				val price=skuDetailsList[0].getPrice();
      				val price=skuDetailsList[0].getOriginalPrice();
      				
      				
      				val flowParams = BillingFlowParams.newBuilder()
      						.setSkuDetails(skuDetailsList[0])
      						.build()
      				billingClient!!.launchBillingFlow([email protected], flowParams)
      			} else {
      				//try to add subscription item "sub_example" in google play console
      				Toast.makeText(applicationContext, "Item not Found", Toast.LENGTH_SHORT).show()
      			}
      		} else {
      			Toast.makeText(applicationContext,
      					" Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
      		}
      	}
      }
      1. Thanks but I still getting some error I’m new to this..
        Can you update full code in above code as MultipleSubscribe.kt

        I read your posts they are best for new android developer like me.. Can you make a separate post specially on consumable product with multiple items in kotlin?

        Thank you

          1. Sure I’ll wait for it.

            One more thing google makes mandatory to Hold and Resubscribe for all, from Nov 1, 2020 in Subscriptions please include this one as well.

            Regards

          2. Here is the Link For consumable product with multiple items in kotlin

            And for

            One more thing google makes mandatory to Hold and Resubscribe for all, from Nov 1, 2020 in Subscriptions please include this one as well.

            Use below Default Subscription Setting
            Account hold : Enabled , Pause : Enabled , Subscription restore : Enabled and Free Trail Limit : One Across All Subscriptions.

            Thanks

Leave a Reply

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