In App Purchase Multiple Non-Consumable In Android/Kotlin

HOW TO MAKE MULTIPLE IN APP PURCHASE IN ANDROID/KOTLIN USING GOOGLE PLAY BILLING LIBRARY


Please Subscribe Youtube| Like Facebook | Follow Twitter

Download Latest Library Code

Introduction

In this article we will learn how to integrate and make in app purchase of Multiple Non-consumable one-time products 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 a listview which shows list of non consumable purchase items and on click of particular item from list allows user to purchase that non consumable item from list. When the particular item is purchased then we will acknowledge that purchase item as well as save its value in preference and refresh the list.

On every app start we will check purchase status of items from Google Play Store Cache and reflect necessary changes accordingly because if user already purchased item previously and re-installs the app or switch to another device or refunded the purchased item, therefore we will store updated purchase status value in user preference.

Google Play Store Cache works best on both offline as well as online. When there is no internet Cache will contain last updated purchase status of item. When internet become available Play Store Cache will be updated automatically with latest purchase status of item. Google Play Store Cache updated roughly every 24 hours or immediately on every purchase.

Requirements:

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

Note: In order to create in app product items in Google Console or perform real test of in app purchase item in your app, you first need to Integrate Google Play Library and add billing permission in your project and then upload that apk on Google play console and publish it any track (Internal test track (recommended), Closed track, Open track, Production track). Therefore we will perform these actions first.

Steps

Follow below steps

1 Integrate Google Play Library in your project

2 Create an application and fill Store listing in Google play console

3 Upload and publish your app

4 Create Non Consumable product items “p1” “p2” “p3” in Google play console

5 Code in app purchase flow logic.

6 Run and Test the application

Step 1 to 3 already explained in this article so follow these steps from there.

4 Create Non Consumable product items in Google play console

After releasing app go to Monetize -> Products -> In-app products.

Click on create and fill necessary info then save and activate the product items. Create three product items and give them id “p1”, “p2”, “p3” respectively.

5 Code in app purchase flow logic.

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

For multiple purchase items we have to make arraylist of purchase item ids and loop through purchase list and make necessary changes. Note that we have assigned preference key and product item id same. You can check these in whole code of MainActivity.

First Add listview in activity_main.xml Layout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</LinearLayout>

In order to make In-App purchase our MainActivity needs to implements PurchasesUpdatedListener interface and override onPurchasesUpdated() method which will be called every time to get notifications for purchases updates. Both purchases initiated by your app and the ones initiated outside of your app will be reported here.

We also need to create BillingClient Object and set listener to it so that we can communicate with Google Play Billing Service.

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 purchase result logic later

    }
}

Now on every item click from list we will initiate purchase flow of that particular item.

listView!!.onItemClickListener = OnItemClickListener { parent, view, position, id ->
	if (getPurchaseItemValueFromPref(purchaseItemIDs[position])) {
		Toast.makeText(applicationContext, purchaseItemIDs[position] + " is Already Purchased", Toast.LENGTH_SHORT).show()
		//selected item is already purchased
		return@OnItemClickListener
	}
	//initiate purchase on selected product item click
	//check if service is already connected
	if (billingClient!!.isReady) {
		initiatePurchase(purchaseItemIDs[position])
	}
	else {
		billingClient = BillingClient.newBuilder(this@MainActivity).enablePendingPurchases().setListener(this@MainActivity).build()
		billingClient!!.startConnection(object : BillingClientStateListener {
			override fun onBillingSetupFinished(billingResult: BillingResult) {
				if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
					initiatePurchase(purchaseItemIDs[position])
				} else {
					Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
				}
			}

			override fun onBillingServiceDisconnected() {}
		})
	}
}

In initiatePurchase() method  we have passed selected product id and specified product type as INAPP. 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(PRODUCT_ID: String) {
	val skuList: MutableList<String> = ArrayList()
	skuList.add(PRODUCT_ID)
	val params = SkuDetailsParams.newBuilder()
	params.setSkusList(skuList).setType(SkuType.INAPP)
	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(this@MainActivity, flowParams)
			}
			else {
				//try to add item/product id "p1" "p2" "p3" inside managed product in google play console
				Toast.makeText(applicationContext, "Purchase Item $PRODUCT_ID not Found", Toast.LENGTH_SHORT).show()
			}
		}
		else {
			Toast.makeText(applicationContext,
					" Error " + billingResult.debugMessage, 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 newly purchased
	if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
		handlePurchases(purchases)
	} else if (billingResult.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
		val queryAlreadyPurchasesResult = billingClient!!.queryPurchases(SkuType.INAPP)
		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. After the purchase, acknowledgement is necessary because failure to properly acknowledge purchase will result in purchase being refunded. After the acknowledgement we will save purchase value in preference and refresh list.

This method also toast user to complete transaction if purchase status is pending. In case of unspecified purchase state we save false value of that product in preference.

fun handlePurchases(purchases: List<Purchase>) {
	for (purchase in purchases) {
		val index = purchaseItemIDs.indexOf(purchase.sku)
		//purchase found
		if (index > -1) {

			//if item is purchased
			if (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()
					continue  //skip current iteration only because other items in purchase list must be checked if present
				}
				// 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
					) { billingResult ->
						if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
							//if purchase is acknowledged
							//then saved value in preference
							savePurchaseItemValueToPref(purchaseItemIDs[index], true)
							Toast.makeText(applicationContext, purchaseItemIDs[index] + " Item Purchased", Toast.LENGTH_SHORT).show()
							notifyList()
						}
					}
				}
				else {
					// Grant entitlement to the user on item purchase
					if (!getPurchaseItemValueFromPref(purchaseItemIDs[index])) {
						savePurchaseItemValueToPref(purchaseItemIDs[index], true)
						Toast.makeText(applicationContext, purchaseItemIDs[index] + " Item Purchased.", Toast.LENGTH_SHORT).show()
						notifyList()
					}
				}
			}
			else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
				Toast.makeText(applicationContext, purchaseItemIDs[index] + " Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show()
			}
			else if (purchase.purchaseState == Purchase.PurchaseState.UNSPECIFIED_STATE) {
				//mark purchase false in case of UNSPECIFIED_STATE
				savePurchaseItemValueToPref(purchaseItemIDs[index], false)
				Toast.makeText(applicationContext, purchaseItemIDs[index] + " Purchase Status Unknown", Toast.LENGTH_SHORT).show()
				notifyList()
			}
		}
	}
}

For verification we have used client side signature verification, which will be performed with in app (less secure). However if you can afford server then you should do verification of purchase through your server (more secure).

In client side verification you need to add your developer’s public key of app.

For old Play Console Design To get key go to Developer Console > Select your app > Development Tools > Services & APIs.

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

Then paste that key to String variable base64key in below method.

/**
 * Verifies that the purchase was signed correctly for this developer's public key.
 *
 * 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 = "Add 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 purchase status of items from Google Play Store Cache using getPurchasesList() method and reflect necessary changes accordingly because if user already purchased item previously and reinstalls the app or switch to another device or refunded the purchased item therefore we should store updated purchase status in user preference. After querying we will call handlePurchases() method which will make necessary changes accordingly. Arraylist purchaseFound is used to loop through each purchase items and update their values in preferences.

billingClient!!.startConnection(object : BillingClientStateListener {
	override fun onBillingSetupFinished(billingResult: BillingResult) {
		if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
			val queryPurchase = billingClient!!.queryPurchases(SkuType.INAPP)
			val queryPurchases = queryPurchase.purchasesList
			if (queryPurchases != null && queryPurchases.size > 0) {
				handlePurchases(queryPurchases)
			}

			//check which items are in purchase list and which are not in purchase list
			//if items that are found add them to purchaseFound
			//check status of found items and save values to preference
			//item which are not found simply save false values to their preference
			//indexOf return index of item in purchase list from 0-2 (because we have 3 items) else returns -1 if not found
			val purchaseFound = ArrayList<Int>()
			if (queryPurchases != null && queryPurchases.size > 0)
			{
				//check item in purchase list
				for (p in queryPurchases) {
					val index = purchaseItemIDs.indexOf(p.sku)
					//if purchase found
					if (index > -1) {
						purchaseFound.add(index)
						if (p.purchaseState == Purchase.PurchaseState.PURCHASED) {
							savePurchaseItemValueToPref(purchaseItemIDs[index], true)
						} else {
							savePurchaseItemValueToPref(purchaseItemIDs[index], false)
						}
					}
				}
				//items that are not found in purchase list mark false
				//indexOf returns -1 when item is not in foundlist
				for (i in purchaseItemIDs.indices) {
					if (purchaseFound.indexOf(i) == -1) {
						savePurchaseItemValueToPref(purchaseItemIDs[i], false)
					}
				}
			}
			else {
				for (purchaseItem in purchaseItemIDs) {
					savePurchaseItemValueToPref(purchaseItem, false)
				}
			}
		}
	}

	override fun onBillingServiceDisconnected() {}
})

Below is the logic of managing multiple items through arraylist and notifyList() method is used to reflect changes in list when it changes.

companion object {
	const val PREF_FILE = "MyPref"

	//note add unique product ids
	//use same id for preference key
	private val purchaseItemIDs: ArrayList<String> = object : ArrayList<String>() {
		init {
			add("p1")
			add("p2")
			add("p3")
		}
	}
	private val purchaseItemDisplay = ArrayList<String>()
}

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

private fun getPurchaseItemValueFromPref(PURCHASE_KEY: String): Boolean {
	return preferenceObject.getBoolean(PURCHASE_KEY, false)
}

private fun savePurchaseItemValueToPref(PURCHASE_KEY: String, value: Boolean) {
	preferenceEditObject.putBoolean(PURCHASE_KEY, value).commit()
}

private fun notifyList() {
	purchaseItemDisplay.clear()
	for (p in purchaseItemIDs) {
		purchaseItemDisplay.add("Purchase Status of " + p + " = " + getPurchaseItemValueFromPref(p))
	}
	arrayAdapter!!.notifyDataSetChanged()
}

Whole Code

Project level build.gradle

buildscript {
    ext.kotlin_version = '1.4.10'
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.2'
        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'

android {
    compileSdkVersion 30
    buildToolsVersion "29.0.3"
    defaultConfig {
        applicationId "com.programtown.example"
        minSdkVersion 17
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation 'com.android.billingclient:billing:3.0.1'
    implementation "androidx.core:core-ktx:1.3.2"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.10"
}
repositories {
    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=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</LinearLayout>

MainActivity.kt

package com.programtown.example

import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import android.os.Bundle
import android.view.View
import android.widget.AdapterView.OnItemClickListener
import android.widget.ArrayAdapter
import android.widget.ListView
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 arrayAdapter: ArrayAdapter<String>? = null
    var listView: ListView? = null
    private var billingClient: BillingClient? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        listView = findViewById<View>(R.id.listview) as ListView

        // Establish connection to billing client
        //check purchase status from google play store cache on every app start
        billingClient = BillingClient.newBuilder(this)
                .enablePendingPurchases().setListener(this).build()

        billingClient!!.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    val queryPurchase = billingClient!!.queryPurchases(SkuType.INAPP)
                    val queryPurchases = queryPurchase.purchasesList
                    if (queryPurchases != null && queryPurchases.size > 0) {
                        handlePurchases(queryPurchases)
                    }

                    //check which items are in purchase list and which are not in purchase list
                    //if items that are found add them to purchaseFound
                    //check status of found items and save values to preference
                    //item which are not found simply save false values to their preference
                    //indexOf return index of item in purchase list from 0-2 (because we have 3 items) else returns -1 if not found
                    val purchaseFound = ArrayList<Int>()
                    if (queryPurchases != null && queryPurchases.size > 0)
                    {
                        //check item in purchase list
                        for (p in queryPurchases) {
                            val index = purchaseItemIDs.indexOf(p.sku)
                            //if purchase found
                            if (index > -1) {
                                purchaseFound.add(index)
                                if (p.purchaseState == Purchase.PurchaseState.PURCHASED) {
                                    savePurchaseItemValueToPref(purchaseItemIDs[index], true)
                                } else {
                                    savePurchaseItemValueToPref(purchaseItemIDs[index], false)
                                }
                            }
                        }
                        //items that are not found in purchase list mark false
                        //indexOf returns -1 when item is not in foundlist
                        for (i in purchaseItemIDs.indices) {
                            if (purchaseFound.indexOf(i) == -1) {
                                savePurchaseItemValueToPref(purchaseItemIDs[i], false)
                            }
                        }
                    }
                    else {
                        for (purchaseItem in purchaseItemIDs) {
                            savePurchaseItemValueToPref(purchaseItem, false)
                        }
                    }
                }
            }

            override fun onBillingServiceDisconnected() {}
        })
        arrayAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, purchaseItemDisplay)
        listView!!.adapter = arrayAdapter
        notifyList()
        listView!!.onItemClickListener = OnItemClickListener { parent, view, position, id ->
            if (getPurchaseItemValueFromPref(purchaseItemIDs[position])) {
                Toast.makeText(applicationContext, purchaseItemIDs[position] + " is Already Purchased", Toast.LENGTH_SHORT).show()
                //selected item is already purchased
                return@OnItemClickListener
            }
            //initiate purchase on selected product item click
            //check if service is already connected
            if (billingClient!!.isReady) {
                initiatePurchase(purchaseItemIDs[position])
            }
            else {
                billingClient = BillingClient.newBuilder(this@MainActivity).enablePendingPurchases().setListener(this@MainActivity).build()
                billingClient!!.startConnection(object : BillingClientStateListener {
                    override fun onBillingSetupFinished(billingResult: BillingResult) {
                        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                            initiatePurchase(purchaseItemIDs[position])
                        } else {
                            Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
                        }
                    }

                    override fun onBillingServiceDisconnected() {}
                })
            }
        }
    }

    private fun notifyList() {
        purchaseItemDisplay.clear()
        for (p in purchaseItemIDs) {
            purchaseItemDisplay.add("Purchase Status of " + p + " = " + getPurchaseItemValueFromPref(p))
        }
        arrayAdapter!!.notifyDataSetChanged()
    }

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

    private fun getPurchaseItemValueFromPref(PURCHASE_KEY: String): Boolean {
        return preferenceObject.getBoolean(PURCHASE_KEY, false)
    }

    private fun savePurchaseItemValueToPref(PURCHASE_KEY: String, value: Boolean) {
        preferenceEditObject.putBoolean(PURCHASE_KEY, value).commit()
    }

    private fun initiatePurchase(PRODUCT_ID: String) {
        val skuList: MutableList<String> = ArrayList()
        skuList.add(PRODUCT_ID)
        val params = SkuDetailsParams.newBuilder()
        params.setSkusList(skuList).setType(SkuType.INAPP)
        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(this@MainActivity, flowParams)
                }
                else {
                    //try to add item/product id "p1" "p2" "p3" inside managed product in google play console
                    Toast.makeText(applicationContext, "Purchase Item $PRODUCT_ID not Found", Toast.LENGTH_SHORT).show()
                }
            }
            else {
                Toast.makeText(applicationContext,
                        " Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
        //if item newly purchased
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            handlePurchases(purchases)
        } else if (billingResult.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
            val queryAlreadyPurchasesResult = billingClient!!.queryPurchases(SkuType.INAPP)
            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) {
            val index = purchaseItemIDs.indexOf(purchase.sku)
            //purchase found
            if (index > -1) {

                //if item is purchased
                if (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()
                        continue  //skip current iteration only because other items in purchase list must be checked if present
                    }
                    // 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
                        ) { billingResult ->
                            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                                //if purchase is acknowledged
                                //then saved value in preference
                                savePurchaseItemValueToPref(purchaseItemIDs[index], true)
                                Toast.makeText(applicationContext, purchaseItemIDs[index] + " Item Purchased", Toast.LENGTH_SHORT).show()
                                notifyList()
                            }
                        }
                    }
                    else {
                        // Grant entitlement to the user on item purchase
                        if (!getPurchaseItemValueFromPref(purchaseItemIDs[index])) {
                            savePurchaseItemValueToPref(purchaseItemIDs[index], true)
                            Toast.makeText(applicationContext, purchaseItemIDs[index] + " Item Purchased.", Toast.LENGTH_SHORT).show()
                            notifyList()
                        }
                    }
                }
                else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
                    Toast.makeText(applicationContext, purchaseItemIDs[index] + " Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show()
                }
                else if (purchase.purchaseState == Purchase.PurchaseState.UNSPECIFIED_STATE) {
                    //mark purchase false in case of UNSPECIFIED_STATE
                    savePurchaseItemValueToPref(purchaseItemIDs[index], false)
                    Toast.makeText(applicationContext, purchaseItemIDs[index] + " Purchase Status Unknown", Toast.LENGTH_SHORT).show()
                    notifyList()
                }
            }
        }
    }

    /**
     * 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 {
            //for old playconsole
            // To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
            //for new play console
            //To get key go to Developer Console > Select your app > Monetize > Monetization setup
            val base64Key = "Add 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"

        //note add unique product ids
        //use same id for preference key
        private val purchaseItemIDs: ArrayList<String> = object : ArrayList<String>() {
            init {
                add("p1")
                add("p2")
                add("p3")
            }
        }
        private val purchaseItemDisplay = ArrayList<String>()
    }
}

Security.java

/*
 * Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.programtown.example;

import android.text.TextUtils;
import android.util.Base64;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;

/**
 * Security-related methods. For a secure implementation, all of this code should be implemented on
 * a server that communicates with the application on the device.
 */
public class Security {
    private static final String TAG = "IABUtil/Security";

    private static final String KEY_FACTORY_ALGORITHM = "RSA";
    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";

    /**
     * Verifies that the data was signed with the given signature, and returns the verified
     * purchase.
     * @param base64PublicKey the base64-encoded public key to use for verifying.
     * @param signedData the signed JSON string (signed, not encrypted)
     * @param signature the signature for the data, signed with the private key
     * @throws IOException if encoding algorithm is not supported or key specification
     * is invalid
     */
    public static boolean verifyPurchase(String base64PublicKey, String signedData,
                                         String signature) throws IOException {
        if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey)
                || TextUtils.isEmpty(signature)) {
            //Purchase verification failed: missing data
            return false;
        }

        PublicKey key = generatePublicKey(base64PublicKey);
        return verify(key, signedData, signature);
    }

    /**
     * Generates a PublicKey instance from a string containing the Base64-encoded public key.
     *
     * @param encodedPublicKey Base64-encoded public key
     * @throws IOException if encoding algorithm is not supported or key specification
     * is invalid
     */
    public static PublicKey generatePublicKey(String encodedPublicKey) throws IOException {
        try {
            byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
        } catch (NoSuchAlgorithmException e) {
            // "RSA" is guaranteed to be available.
            throw new RuntimeException(e);
        } catch (InvalidKeySpecException e) {
            String msg = "Invalid key specification: " + e;
            throw new IOException(msg);
        }
    }

    /**
     * Verifies that the signature from the server matches the computed signature on the data.
     * Returns true if the data is correctly signed.
     *
     * @param publicKey public key associated with the developer account
     * @param signedData signed data from server
     * @param signature server signature
     * @return true if the data and signature match
     */
    public static boolean verify(PublicKey publicKey, String signedData, String signature) {
        byte[] signatureBytes;
        try {
            signatureBytes = Base64.decode(signature, Base64.DEFAULT);
        } catch (IllegalArgumentException e) {
            //Base64 decoding failed
            return false;
        }
        try {
            Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM);
            signatureAlgorithm.initVerify(publicKey);
            signatureAlgorithm.update(signedData.getBytes());
            if (!signatureAlgorithm.verify(signatureBytes)) {
                //Signature verification failed
                return false;
            }
            return true;
        } catch (NoSuchAlgorithmException e) {
            // "RSA" is guaranteed to be available
            throw new RuntimeException(e);
        } catch (InvalidKeyException e) {
            //Invalid key specification
        } catch (SignatureException e) {
            //Signature exception
        }
        return false;
    }
}

6 Run and Test the application

Now Run your app and test in app purchase by using your gmail account that is associated with your Google Console developer account. Because by default, the only test account registered is the one that’s associated with your developer account. You can register additional test accounts by using the Google Play Console.

Conclusion

So in this article we have learnt how to integrate and make in app purchase of multiple non consumable products. If you liked the article then please share this page and article. Thanks.

NOTE:

Need Support?

References:

https://developer.android.com/google/play/billing/billing_overview

Please Subscribe Youtube| Like Facebook | Follow Twitter


Leave a Reply

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