In App Purchase Single Non-Consumable In Android/Kotlin

HOW TO MAKE 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 Single Non-consumable one-time products using google play billing library.

Orange Pi 800 Development Board 4GB RAM 64GB

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

In-app product types

Google Play Billing can be used to sell the following types of in-app products:

1)  One-time products: An in-app product requiring a single, non-recurring charge to the user’s form of payment. Additional game levels, premium loot boxes, and media files are examples of one-time products. The Google Play Console refers to one-time products as managed products, and the Google Play Billing library calls them “INAPP”.

Google Play Billing supports the following types of one-time products:

  • Non-consumable one-time products are products that provide a permanent effect, such as a premium upgrade. To avoid users from repurchasing these products, you shouldn’t indicate them as being consumed.
  • Consumable one-time products are products that provide temporary benefits and can be repurchased, such as additional in-game currency or extra game lives. To make a consumable one-time product available for purchase again, you need to send a consumption request to Google Play.

2)  Subscriptions: An in-app product requiring a recurring charge to the user’s form of payment. Online service is example of subscription. The Google Play Billing Library calls these “SUBS”.

Our Example

In this post we will demonstrate how to purchase Non-consumable one-time products. For example purpose we will use “purchase” as Non-consumable one-time product which will allow user to purchase it one time only. You can use this according to your need such as to remove ads or upgrade premium status etc.

In our example we have a textview which shows purchase status and a purchase button which allow user to purchase item. When item is bought purchase status will be changed to “purchased” and purchase button will be removed and we store purchase value in user preference.

On every app start we will check purchase status of item 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 product item “purchase” One-Time Managed Product in Google play console

5 Code in app purchase flow logic.

6 Run and Test the application

1 Integrate Google Play Billing Library in your project.

First add Google Play Billing Library dependency at app level of your build.gradle file.

implementation 'com.android.billingclient:billing:3.0.1'

In manifest file add below permission

<uses-permission android:name="com.android.vending.BILLING" />

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

First go to your Google play console and create an application

After creating App fill necessary Store listing

3 Upload and publish your app

After necessary Store listing is filled go to app release and open internal test track and click on create release.

Upload your apk, fill necessary info then review and publish/release your app in internal test track.

4 Create product item “purchase” One-Time Managed Product in Google play console

After releasing app in internal test go to Store presence -> In-app products -> Managed products

Click on create managed product

Enter product id “purchase”, title, description and click on active.

Add price details for demo purpose we have entered rs 500 PKR

Then click on save

After saving product item will be created

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.

Then Add textview Purchase Status and Purchase 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"
    android:orientation="vertical"
    tools:context="com.programtown.example.MainActivity">

    <TextView
        android:id="@+id/purchase_status"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"

          />

    <Button
        android:id="@+id/purchase_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Purchase"
        android:onClick="purchase"
    />
</LinearLayout>

In order to make In-App purchase our MainActivity needs to implements PurchasesUpdatedListener interface and override onPurchasesUpdated() function 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

    }
}

On purchase button click we will initiate purchase flow

//initiate purchase on button click
fun purchase(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(object : BillingClientStateListener {
			override fun onBillingSetupFinished(billingResult: BillingResult) {
				if (billingResult.responseCode == BillingResponseCode.OK) {
					initiatePurchase()
				} else {
					Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
				}
			}

			override fun onBillingServiceDisconnected() {}
		})
	}
}

In initiatePurchase() function we have included product id “purchase”, which was added in Google Play Console inside in app products, specified product type as INAPP. Then we have called querySkuDetailsAsync() function which will query our product from Play console. After querying we will call launchBillingFlow() function which will display purchase dialog. Result of purchase dialog will be reported inside onPurchasesUpdated() function which we will code next.

private fun initiatePurchase() {

	val skuList: MutableList<String> = ArrayList()
	skuList.add(PRODUCT_ID)
	val params = SkuDetailsParams.newBuilder()
	params.setSkusList(skuList).setType(INAPP)

	billingClient!!.querySkuDetailsAsync(params.build())
	{ billingResult, skuDetailsList ->
		if (billingResult.responseCode == 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 "purchase" inside managed product in google play console

				Toast.makeText(applicationContext, "Purchase Item not Found", Toast.LENGTH_SHORT).show()
			}
		} else {
			Toast.makeText(applicationContext,
					" Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
		}
	}
}

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

override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
	//if item newly purchased

	if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) {
		handlePurchases(purchases)
	}
	//if item already purchased then check and reflect changes
	else if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
		val queryAlreadyPurchasesResult = billingClient!!.queryPurchases(INAPP)
		val alreadyPurchases: List<Purchase>? = queryAlreadyPurchasesResult.purchasesList
		if (alreadyPurchases != null) {
			handlePurchases(alreadyPurchases)
		}
	}
	//if purchase cancelled
	else if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) {
		Toast.makeText(applicationContext, "Purchase Canceled", Toast.LENGTH_SHORT).show()
	}
	// Handle any other error msgs
	else {
		Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
	}
}

In handlePurchases() function 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 restart activity to make necessary changes e.g. change purchase status, hide purchase button.

This function also toast user to complete transaction if purchase status is pending. In case of unspecified purchase state we will change purchase status to “Not Purchased” and store updated value in preference.

fun handlePurchases(purchases: List<Purchase>) {
	for (purchase in purchases) {
		//if item is purchased

		if (PRODUCT_ID == purchase.sku && purchase.purchaseState == 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 item is purchased and also acknowledged
			else {
				// Grant entitlement to the user on item purchase
				// restart activity

				if (!purchaseValueFromPref) {
					savePurchaseValueToPref(true)
					Toast.makeText(applicationContext, "Item Purchased", Toast.LENGTH_SHORT).show()
					recreate()
				}
			}
		}
		//if purchase is pending
		else if (PRODUCT_ID == purchase.sku && purchase.purchaseState == PurchaseState.PENDING) {
			Toast.makeText(applicationContext,
					"Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show()
		}
		//if purchase is refunded or unknown
		else if (PRODUCT_ID == purchase.sku && purchase.purchaseState == PurchaseState.UNSPECIFIED_STATE) {
			savePurchaseValueToPref(false)
			purchaseStatus!!.text = "Purchase Status : Not Purchased"
			purchaseButton!!.visibility = View.VISIBLE
			Toast.makeText(applicationContext, "Purchase Status Unknown", Toast.LENGTH_SHORT).show()
		}
	}
}

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

		savePurchaseValueToPref(true)
		Toast.makeText(applicationContext, "Item Purchased", Toast.LENGTH_SHORT).show()
		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 function.

/**
 * 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 item from Google Play Store Cache using getPurchasesList() function 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() function which will make necessary changes accordingly.

billingClient = BillingClient.newBuilder(this)
		.enablePendingPurchases().setListener(this).build()
billingClient!!.startConnection(object : BillingClientStateListener {
	override fun onBillingSetupFinished(billingResult: BillingResult) {
		if (billingResult.responseCode == BillingResponseCode.OK) {
			val queryPurchase = billingClient!!.queryPurchases(INAPP)
			val queryPurchases: List<Purchase>? = queryPurchase.purchasesList
			if (queryPurchases != null && queryPurchases.size > 0) {
				handlePurchases(queryPurchases)
			}
			//if purchase list is empty that means item is not purchased
			//Or purchase is refunded or canceled
			else{
				savePurchaseValueToPref(false);
			}
		}
	}

	override fun onBillingServiceDisconnected() {}
})

//item Purchased
if (purchaseValueFromPref) {
	purchaseButton!!.visibility = View.GONE
	purchaseStatus!!.text = "Purchase Status : Purchased"
}
//item not Purchased
else {
	purchaseButton!!.visibility = View.VISIBLE
	purchaseStatus!!.text = "Purchase Status : Not Purchased"
}

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

    <TextView
        android:id="@+id/purchase_status"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"

          />

    <Button
        android:id="@+id/purchase_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Purchase"
        android:onClick="purchase"
    />

</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.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.BillingResponseCode
import com.android.billingclient.api.BillingClient.SkuType.INAPP
import com.android.billingclient.api.Purchase.PurchaseState
import com.programtown.example.R.id
import com.programtown.example.R.layout
import java.io.IOException
import java.util.*

class MainActivity : AppCompatActivity(), PurchasesUpdatedListener {

    var purchaseStatus: TextView? = null
    var purchaseButton: Button? = null
    private var billingClient: BillingClient? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(layout.activity_main)

        purchaseStatus = findViewById<View>(id.purchase_status) as TextView
        purchaseButton = findViewById<View>(id.purchase_button) as Button

        billingClient = BillingClient.newBuilder(this)
                .enablePendingPurchases().setListener(this).build()
        billingClient!!.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingResponseCode.OK) {
                    val queryPurchase = billingClient!!.queryPurchases(INAPP)
                    val queryPurchases: List<Purchase>? = queryPurchase.purchasesList
                    if (queryPurchases != null && queryPurchases.size > 0) {
                        handlePurchases(queryPurchases)
                    }
                    //if purchase list is empty that means item is not purchased
                    //Or purchase is refunded or canceled
                    else{
                        savePurchaseValueToPref(false);
                    }
                }
            }

            override fun onBillingServiceDisconnected() {}
        })

        //item Purchased
        if (purchaseValueFromPref) {
            purchaseButton!!.visibility = View.GONE
            purchaseStatus!!.text = "Purchase Status : Purchased"
        }
        //item not Purchased
        else {
            purchaseButton!!.visibility = View.VISIBLE
            purchaseStatus!!.text = "Purchase Status : Not Purchased"
        }
    }

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

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

    private val purchaseValueFromPref: Boolean
        get() = preferenceObject.getBoolean(PURCHASE_KEY, false)

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

    //initiate purchase on button click
    fun purchase(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(object : BillingClientStateListener {
                override fun onBillingSetupFinished(billingResult: BillingResult) {
                    if (billingResult.responseCode == BillingResponseCode.OK) {
                        initiatePurchase()
                    } else {
                        Toast.makeText(applicationContext, "Error " + billingResult.debugMessage, Toast.LENGTH_SHORT).show()
                    }
                }

                override fun onBillingServiceDisconnected() {}
            })
        }
    }

    private fun initiatePurchase() {

        val skuList: MutableList<String> = ArrayList()
        skuList.add(PRODUCT_ID)
        val params = SkuDetailsParams.newBuilder()
        params.setSkusList(skuList).setType(INAPP)

        billingClient!!.querySkuDetailsAsync(params.build())
        { billingResult, skuDetailsList ->
            if (billingResult.responseCode == 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 "purchase" inside managed product in google play console

                    Toast.makeText(applicationContext, "Purchase Item 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 == BillingResponseCode.OK && purchases != null) {
            handlePurchases(purchases)
        }
        //if item already purchased then check and reflect changes
        else if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
            val queryAlreadyPurchasesResult = billingClient!!.queryPurchases(INAPP)
            val alreadyPurchases: List<Purchase>? = queryAlreadyPurchasesResult.purchasesList
            if (alreadyPurchases != null) {
                handlePurchases(alreadyPurchases)
            }
        }
        //if purchase cancelled
        else if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) {
            Toast.makeText(applicationContext, "Purchase Canceled", Toast.LENGTH_SHORT).show()
        }
        // Handle any other error msgs
        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 (PRODUCT_ID == purchase.sku && purchase.purchaseState == 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 item is purchased and also acknowledged
                else {
                    // Grant entitlement to the user on item purchase
                    // restart activity

                    if (!purchaseValueFromPref) {
                        savePurchaseValueToPref(true)
                        Toast.makeText(applicationContext, "Item Purchased", Toast.LENGTH_SHORT).show()
                        recreate()
                    }
                }
            }
            //if purchase is pending
            else if (PRODUCT_ID == purchase.sku && purchase.purchaseState == PurchaseState.PENDING) {
                Toast.makeText(applicationContext,
                        "Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show()
            }
            //if purchase is refunded or unknown
            else if (PRODUCT_ID == purchase.sku && purchase.purchaseState == PurchaseState.UNSPECIFIED_STATE) {
                savePurchaseValueToPref(false)
                purchaseStatus!!.text = "Purchase Status : Not Purchased"
                purchaseButton!!.visibility = View.VISIBLE
                Toast.makeText(applicationContext, "Purchase Status Unknown", Toast.LENGTH_SHORT).show()
            }
        }
    }

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

            savePurchaseValueToPref(true)
            Toast.makeText(applicationContext, "Item Purchased", Toast.LENGTH_SHORT).show()
            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 = "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"
        const val PURCHASE_KEY = "purchase"
        const val PRODUCT_ID = "purchase"
    }
}

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 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 one time product. 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

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

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


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

Leave a Reply

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