From 5bd56ac7cd139874dd977d19eb52932a233eb183 Mon Sep 17 00:00:00 2001 From: hannanshahid0 Date: Wed, 14 Jun 2023 11:34:48 +0500 Subject: [PATCH] v 1.0.3 - Billing lib 6.0.0 updated - Implemented consumable one-time products - Billing Client Ready/Error Callbacks Added - Set Logging for Release or Debug (By default only logs on debug mode) - Now initialize billing lib in App class (if you want) - Billing client ready check issue solved --- README.md | 86 +++++++++++++++-- app/build.gradle | 2 + .../funsolbillinghelper/MainActivity.kt | 13 ++- funsol-billing-utils/build.gradle | 6 +- .../iap/billing/BillingClientListener.kt | 9 ++ .../iap/billing/BillingEventListener.kt | 1 + .../funsol/iap/billing/FunSolBillingHelper.kt | 92 ++++++++++++++----- .../funsol/iap/billing/model/ErrorType.java | 3 +- 8 files changed, 173 insertions(+), 39 deletions(-) create mode 100644 funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingClientListener.kt diff --git a/README.md b/README.md index 2d2a9cb..a21f7ea 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![](https://jitpack.io/v/Funsol-Projects/Funsol-Billing-Helper.svg)](https://jitpack.io/#Funsol-Projects/Funsol-Billing-Helper) -Funsol Billing Helper is a simple, straight-forward implementation of the Android v5.1 In-app billing API +Funsol Billing Helper is a simple, straight-forward implementation of the Android v6.0 In-app billing API > Support both IN-App and Subscriptions. -### **Billing v5 subscription model:** +### **Billing v6 subscription model:** ![Subcription](https://user-images.githubusercontent.com/106656179/227849820-8b9e8566-fa6e-40d4-862e-77aaeaa65e6c.png) @@ -36,10 +36,10 @@ Add Funsol Billing Helper dependencies in App level build.gradle. ```kotlin - dependencies { - implementation 'com.github.Funsol-Projects:Funsol-Billing-Helper:v1.0.2' - } - +dependencies { + implementation 'com.github.Funsol-Projects:Funsol-Billing-Helper:v1.0.3' +} + ``` ## Step 3 (Setup) @@ -61,11 +61,43 @@ if both subscription and In-App .setInAppKeys(mutableListOf("In-App Key")) ``` +if consumable in-App +```kotlin -Call this in first stable activity + FunSolBillingHelper(this) + .setInAppKeys(mutableListOf("In-App Key, In-App consumable Key")) + .setConsumableKeys(mutableListOf("In-App consumable Key")) + +``` +**Note: you have add consumable key in both func ```setInAppKeys()``` and ```setConsumableKeys()```** + +Call this in first stable activity or in App class + +### Billing Client Listeners + +```kotlin + + FunSolBillingHelper(this) + .setSubKeys(mutableListOf("Subs Key", "Subs Key 2")) + .setInAppKeys(mutableListOf("In-App Key")) + .enableLogging() + .setBillingClientListener(object : BillingClientListener { + override fun onClientReady() { + Log.i("billing", "onClientReady: ") + } + + override fun onClientInitError() { + Log.i("billing", "onClientInitError: ") + } + + }) + + +``` ### Enable Logs +##### Only for debug ```kotlin @@ -76,11 +108,25 @@ Call this in first stable activity ``` + +##### For both Debug/Release + +```kotlin + + FunSolBillingHelper(this) + .setSubKeys(mutableListOf("Subs Key", "Subs Key 2")) + .setInAppKeys(mutableListOf("In-App Key")) + .enableLogging(isEnableWhileRelease = true) + + +``` + + ### Buy In-App Product Subscribe to a Subscription ```kotlin - FunSolBillingHelper(this).buyInApp("In-App Key",false) + FunSolBillingHelper(this).buyInApp(activity,"In-App Key",false) ``` ```fasle``` value used for **isPersonalizedOffer** attribute: @@ -91,11 +137,11 @@ If your app can be distributed to users in the European Union, use the **isPerso Subscribe to a Subscription ```kotlin - FunSolBillingHelper(this).subscribe(this, "Base Plan ID") + FunSolBillingHelper(this).subscribe(activity, "Base Plan ID") ``` Subscribe to a offer ```kotlin - FunSolBillingHelper(this).subscribe(this, "Base Plan ID","Offer ID") + FunSolBillingHelper(this).subscribe(activity, "Base Plan ID","Offer ID") ``` **Note: it auto acknowledge the subscription and give callback when product acknowledged successfully.** @@ -147,9 +193,15 @@ Interface implementation to handle purchase results and errors. FunSolBillingHelper(this).setBillingEventListener(object : BillingEventListener { override fun onProductsPurchased(purchases: List) { + //call back when purchase occured } override fun onPurchaseAcknowledged(purchase: Purchase) { + //call back when purchase occur and acknowledged + } + + override fun onPurchaseConsumed(purchase: Purchase) { + //call back when purchase occur and consumed } override fun onBillingError(error: ErrorType) { @@ -205,6 +257,10 @@ Interface implementation to handle purchase results and errors. ErrorType.OLD_PURCHASE_TOKEN_NOT_FOUND -> { + } + + ErrorType.CONSUME_ERROR -> { + } else -> { @@ -344,6 +400,16 @@ FunSolBillingHelper(this).release() ``` This Method used for Releasing the client object and save from memory leaks +## CHANGELOG + +- 14-06-2023 + - Billing lib 6.0.0 updated + - Implemented consumable one-time products + - Billing Client Ready/Error Callbacks Added + - Set Logging for Release or Debug (By default only logs on debug mode) + - Now initialize billing lib in App class (if you want) + - Billing client ready check issue solved + ## License diff --git a/app/build.gradle b/app/build.gradle index 7fbf222..fd12f4c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,5 +41,7 @@ dependencies { implementation project(path: ':funsol-billing-utils') testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' + implementation 'com.android.billingclient:billing:6.0.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } \ No newline at end of file diff --git a/app/src/main/java/com/billing/funsolbillinghelper/MainActivity.kt b/app/src/main/java/com/billing/funsolbillinghelper/MainActivity.kt index 7f859d4..312ca23 100644 --- a/app/src/main/java/com/billing/funsolbillinghelper/MainActivity.kt +++ b/app/src/main/java/com/billing/funsolbillinghelper/MainActivity.kt @@ -2,6 +2,8 @@ package com.billing.funsolbillinghelper import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.util.Log +import com.funsol.iap.billing.BillingClientListener import com.funsol.iap.billing.FunSolBillingHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -11,7 +13,16 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - FunSolBillingHelper(this).setSubKeys(mutableListOf("basic")) + FunSolBillingHelper(this).setSubKeys(mutableListOf("basic")).enableLogging(isEnableWhileRelease = true).setBillingClientListener(object : BillingClientListener { + override fun onClientReady() { + Log.i("billing", "onClientReady: ") + } + + override fun onClientInitError() { + Log.i("billing", "onClientInitError: ") + } + + }) } } \ No newline at end of file diff --git a/funsol-billing-utils/build.gradle b/funsol-billing-utils/build.gradle index 590ad27..ad02744 100644 --- a/funsol-billing-utils/build.gradle +++ b/funsol-billing-utils/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.android.billingclient:billing:5.1.0' + implementation 'com.android.billingclient:billing:6.0.0' } afterEvaluate { @@ -49,13 +49,13 @@ afterEvaluate { from components.release groupId = 'com.github.Funsol-Projects' artifactId = 'Funsol-Billing-Helper' - version = 'v1.0.2' + version = 'v1.0.3' } debug(MavenPublication) { from components.debug groupId = 'com.github.Funsol-Projects' artifactId = 'Funsol-Billing-Helper' - version = 'v1.0.2' + version = 'v1.0.3' } } } diff --git a/funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingClientListener.kt b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingClientListener.kt new file mode 100644 index 0000000..a377401 --- /dev/null +++ b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingClientListener.kt @@ -0,0 +1,9 @@ +package com.funsol.iap.billing + +import com.android.billingclient.api.Purchase +import com.funsol.iap.billing.model.ErrorType + +interface BillingClientListener { + fun onClientReady() + fun onClientInitError() +} \ No newline at end of file diff --git a/funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingEventListener.kt b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingEventListener.kt index ba10c85..de33a84 100644 --- a/funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingEventListener.kt +++ b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/BillingEventListener.kt @@ -7,5 +7,6 @@ import com.funsol.iap.billing.model.ErrorType interface BillingEventListener { fun onProductsPurchased(purchases: List) fun onPurchaseAcknowledged(purchase: Purchase) + fun onPurchaseConsumed(purchase: Purchase) fun onBillingError(error: ErrorType) } \ No newline at end of file diff --git a/funsol-billing-utils/src/main/java/com/funsol/iap/billing/FunSolBillingHelper.kt b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/FunSolBillingHelper.kt index c5ff1a2..319d3b7 100644 --- a/funsol-billing-utils/src/main/java/com/funsol/iap/billing/FunSolBillingHelper.kt +++ b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/FunSolBillingHelper.kt @@ -1,6 +1,7 @@ package com.funsol.iap.billing import android.app.Activity +import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log @@ -12,24 +13,27 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class FunSolBillingHelper(private val activity: Activity) { +class FunSolBillingHelper(private val context: Context) { private val TAG = "FunSolBillingHelper" - private var isClientReady = false companion object { + private var isClientReady = false private var billingClient: BillingClient? = null private var billingEventListener: BillingEventListener? = null + private var billingClientListener: BillingClientListener? = null private var purchasesUpdatedListener: PurchasesUpdatedListener? = null private val subKeys by lazy { mutableListOf() } private val inAppKeys by lazy { mutableListOf() } + private val consumeAbleKeys by lazy { mutableListOf() } private val AllProducts by lazy { mutableListOf() } private val purchasedProductList by lazy { mutableListOf() } private val purchasedInAppProductList by lazy { mutableListOf() } private var enableLog = false + private var enableLogWhileRelease = false } init { @@ -95,7 +99,7 @@ class FunSolBillingHelper(private val activity: Activity) { } } } - billingClient = BillingClient.newBuilder(activity.applicationContext).setListener(purchasesUpdatedListener!!).enablePendingPurchases().build() + billingClient = BillingClient.newBuilder(context).setListener(purchasesUpdatedListener!!).enablePendingPurchases().build() startConnection() } else { Log("Client already connected") @@ -110,6 +114,7 @@ class FunSolBillingHelper(private val activity: Activity) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { Log("Connected to Google Play") isClientReady = true + billingClientListener?.onClientReady() fetchAvailableAllSubsProducts(subKeys) fetchAvailableAllInAppProducts(inAppKeys) CoroutineScope(Dispatchers.IO).launch { @@ -121,6 +126,7 @@ class FunSolBillingHelper(private val activity: Activity) { override fun onBillingServiceDisconnected() { Log("Fail to connect with Google Play") + billingClientListener?.onClientInitError() isClientReady = false } }) @@ -317,7 +323,7 @@ class FunSolBillingHelper(private val activity: Activity) { //////////////////////////////////////////////////// In-App ///////////////////////////////////////////////////////////// - fun buyInApp(productId: String, isPersonalizedOffer: Boolean = false) { + fun buyInApp(activity: Activity, productId: String, isPersonalizedOffer: Boolean = false) { if (billingClient != null) { val productInfo = getProductDetail(productId, "", BillingClient.ProductType.INAPP) if (productInfo != null) { @@ -327,13 +333,13 @@ class FunSolBillingHelper(private val activity: Activity) { val billingFlowParams = BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParamsList).setIsOfferPersonalized(isPersonalizedOffer).build() billingClient!!.launchBillingFlow(activity, billingFlowParams) - Log("Buying INAPP : $productId") + Log("Buying IN-APP : $productId") } else { billingEventListener?.onBillingError(ErrorType.PRODUCT_NOT_EXIST) - Log("Billing client can not launch billing flow because INAPP product details are missing") + Log("Billing client can not launch billing flow because IN-APP product details are missing") } } else { - Log("Billing client null while purchases INAPP") + Log("Billing client null while purchases IN-APP") billingEventListener?.onBillingError(ErrorType.SERVICE_DISCONNECTED) } } @@ -358,7 +364,7 @@ class FunSolBillingHelper(private val activity: Activity) { val productList = mutableListOf() productListKeys.forEach { - Log("SS in-App keys List ${productListKeys.size} $it") + Log("in-App keys List ${productListKeys.size} $it") productList.add(QueryProductDetailsParams.Product.newBuilder().setProductId(it).setProductType(BillingClient.ProductType.INAPP).build()) } val queryProductDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(productList).build() @@ -366,17 +372,17 @@ class FunSolBillingHelper(private val activity: Activity) { if (billingClient != null) { billingClient!!.queryProductDetailsAsync(queryProductDetailsParams) { billingResult, productDetailsList -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { - Log("SS in productDetailsList ${productDetailsList.size}") + Log("in productDetailsList ${productDetailsList.size}") productDetailsList.forEach { productDetails -> Log("SS in app product details ${productDetails.toString()}") AllProducts.add(productDetails) } } else { - Log("SS Failed to retrieve In-APP Prices ${billingResult.debugMessage}") + Log("Failed to retrieve In-APP Prices ${billingResult.debugMessage}") } } } else { - Log("SS Billing client null while fetching All In-App Products") + Log("Billing client null while fetching All In-App Products") billingEventListener?.onBillingError(ErrorType.SERVICE_DISCONNECTED) } } @@ -401,6 +407,11 @@ class FunSolBillingHelper(private val activity: Activity) { return this } + fun setConsumableKeys(keysList: MutableList): FunSolBillingHelper { + consumeAbleKeys.addAll(keysList) + return this + } + ///////////////////////////////////////////////// Common //////////////////////////////////////////////////////////// fun getAllProductPrices(): MutableList { @@ -488,7 +499,7 @@ class FunSolBillingHelper(private val activity: Activity) { } } } - Log("InAPP Product Price not found because product is missing") + Log("IN-APP Product Price not found because product is missing") billingEventListener?.onBillingError(ErrorType.PRODUCT_NOT_EXIST) return null } @@ -498,12 +509,13 @@ class FunSolBillingHelper(private val activity: Activity) { val productType = getProductType(purchase.products.first()) if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { if (!purchase.isAcknowledged) { + val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken) billingClient!!.acknowledgePurchase(acknowledgePurchaseParams.build()) { if (it.responseCode == BillingClient.BillingResponseCode.OK) { if (productType.trim().isNotEmpty()) { if (productType == BillingClient.ProductType.INAPP) { - Log("INAPP item buy after acknowledge ") + Log("IN-APP item buy after acknowledge ") purchasedInAppProductList.add(PurchaseHistoryRecord(purchase.originalJson, purchase.signature)) } else { Log("SUBS item buy after acknowledge ") @@ -511,7 +523,6 @@ class FunSolBillingHelper(private val activity: Activity) { } } else { Log("Product type not found while handle purchases") - } billingEventListener?.onPurchaseAcknowledged(purchase) } else { @@ -525,6 +536,26 @@ class FunSolBillingHelper(private val activity: Activity) { purchasedProductList.add(purchase) } + + if (consumeAbleKeys.isNotEmpty()) { + if (consumeAbleKeys.contains(purchase.products.first())) { + Log("this purchase is consumable") + + val consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build() + billingClient?.consumeAsync(consumeParams) { result, str -> + if (result.responseCode == BillingClient.BillingResponseCode.OK) { + Log("Purchase consumed") + billingEventListener?.onPurchaseConsumed(purchase) + } else { + Log("Purchase fail to consume") + billingEventListener?.onBillingError(ErrorType.CONSUME_ERROR) + } + + } + } else { + Log("this purchase is not consumable") + } + } } else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) { Log( "Handling acknowledges: purchase can not be acknowledged because the state is PENDING. " + "A purchase can be acknowledged only when the state is PURCHASED" @@ -548,11 +579,10 @@ class FunSolBillingHelper(private val activity: Activity) { private suspend fun fetchActiveSubsPurchases() { if (billingClient != null) { - billingClient!!.queryPurchasesAsync( - QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build() - ) { billingResult: BillingResult, purchases: List -> + billingClient!!.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()) { billingResult: BillingResult, purchases: List -> + Log("BillingResult $billingResult") if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { Log("SUBS item already buy founded list size ${purchases.size}") val activePurchases = purchases.filter { purchase -> @@ -575,19 +605,19 @@ class FunSolBillingHelper(private val activity: Activity) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { - Log("INAPP item already buy founded list size ${purchases.size}") + Log("IN-APP item already buy founded list size ${purchases.size}") val activePurchases = purchases.filter { purchase -> purchase.purchaseState == Purchase.PurchaseState.PURCHASED } - Log("active INAPP item already buy founded list size ${activePurchases.size}") + Log("active IN-APP item already buy founded list size ${activePurchases.size}") CoroutineScope(Dispatchers.IO).launch { activePurchases.forEach { purchase -> - Log("INAPP item already buy founded item:${purchase.products.first()}") + Log("IN-APP item already buy founded item:${purchase.products.first()}") handlePurchase(purchase) } } } else { - Log("no INAPP item already buy") + Log("no IN-APP item already buy") } } } else { @@ -650,14 +680,22 @@ class FunSolBillingHelper(private val activity: Activity) { return isClientReady } - fun enableLogging(): FunSolBillingHelper { + fun enableLogging(isEnableWhileRelease: Boolean = false): FunSolBillingHelper { enableLog = true + enableLogWhileRelease = isEnableWhileRelease return this } private fun Log(debugMessage: String) { + if (enableLog) { - Log.d(TAG, debugMessage) + if (enableLogWhileRelease) { + Log.d(TAG, debugMessage) + } else { + if (BuildConfig.DEBUG) { + Log.d(TAG, debugMessage) + } + } } } @@ -668,7 +706,13 @@ class FunSolBillingHelper(private val activity: Activity) { } } - fun setBillingEventListener(billingEventListeners: BillingEventListener?) { + fun setBillingEventListener(billingEventListeners: BillingEventListener?): FunSolBillingHelper { billingEventListener = billingEventListeners + return this + } + + fun setBillingClientListener(billingClientListeners: BillingClientListener?): FunSolBillingHelper { + billingClientListener = billingClientListeners + return this } } \ No newline at end of file diff --git a/funsol-billing-utils/src/main/java/com/funsol/iap/billing/model/ErrorType.java b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/model/ErrorType.java index 8c6c586..7e28de2 100644 --- a/funsol-billing-utils/src/main/java/com/funsol/iap/billing/model/ErrorType.java +++ b/funsol-billing-utils/src/main/java/com/funsol/iap/billing/model/ErrorType.java @@ -22,5 +22,6 @@ public enum ErrorType { SERVICE_DISCONNECTED, ACKNOWLEDGE_ERROR, ACKNOWLEDGE_WARNING, - OLD_PURCHASE_TOKEN_NOT_FOUND, INVALID_PRODUCT_TYPE_SET + OLD_PURCHASE_TOKEN_NOT_FOUND, INVALID_PRODUCT_TYPE_SET, + CONSUME_ERROR } \ No newline at end of file