From c3308a911ca10d0a23ea5295fdf57a455ff80b74 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 3 Jul 2023 13:12:25 +0530 Subject: [PATCH 1/9] Implemented non subscription purchase method and validation --- .../example/billing/BillingActivity.java | 61 +++-- .../example/billing/BillingViewModel.kt | 64 ++++- .../res/layout/dialog_customer_layout.xml | 21 +- .../java/com/chargebee/android/Chargebee.kt | 2 - .../billingservice/BillingClientManager.kt | 234 +++++++++++++++--- .../android/billingservice/CBCallback.kt | 5 + .../android/billingservice/CBPurchase.kt | 105 ++++++-- .../billingservice/OneTimeProductType.kt | 7 + .../android/billingservice/ProductType.kt | 6 + .../models/CBNonSubscriptionResponse.kt | 17 ++ .../com/chargebee/android/models/Products.kt | 10 +- .../android/network/CBReceiptRequestBody.kt | 45 +++- .../android/repository/ReceiptRepository.kt | 10 + .../android/resources/ReceiptResource.kt | 18 +- .../restore/CBRestorePurchaseManager.kt | 38 ++- .../BillingClientManagerTest.kt | 23 +- .../android/restore/RestorePurchaseTest.kt | 22 +- 17 files changed, 576 insertions(+), 112 deletions(-) create mode 100644 chargebee/src/main/java/com/chargebee/android/billingservice/OneTimeProductType.kt create mode 100644 chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt create mode 100644 chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt diff --git a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java index a8fda6d..8229f02 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -9,8 +9,9 @@ import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.chargebee.android.ProgressBarListener; +import com.chargebee.android.billingservice.OneTimeProductType; +import com.chargebee.android.billingservice.ProductType; import com.chargebee.android.models.CBProduct; import com.chargebee.android.network.CBCustomer; import com.chargebee.example.BaseActivity; @@ -32,6 +33,7 @@ public class BillingActivity extends BaseActivity implements ProductListAdapter. private static final String TAG = "BillingActivity"; private int position = 0; CBCustomer cbCustomer; + private EditText inputProductType; @Override protected void onCreate(Bundle savedInstanceState) { @@ -125,31 +127,62 @@ private void getCustomerID() { EditText inputFirstName = dialog.findViewById(R.id.firstNameText); EditText inputLastName = dialog.findViewById(R.id.lastNameText); EditText inputEmail = dialog.findViewById(R.id.emailText); + inputProductType = dialog.findViewById(R.id.productTypeText); + if (isOneTimeProduct()) inputProductType.setVisibility(View.GONE); + else inputProductType.setVisibility(View.VISIBLE); Button dialogButton = dialog.findViewById(R.id.btn_ok); dialogButton.setText("Ok"); - dialogButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showProgressDialog(); - String customerId = input.getText().toString(); - String firstName = inputFirstName.getText().toString(); - String lastName = inputLastName.getText().toString(); - String email = inputEmail.getText().toString(); - cbCustomer = new CBCustomer(customerId,firstName,lastName,email); + dialogButton.setOnClickListener(view -> { + String customerId = input.getText().toString(); + String firstName = inputFirstName.getText().toString(); + String lastName = inputLastName.getText().toString(); + String email = inputEmail.getText().toString(); + String productType = inputProductType.getText().toString(); + cbCustomer = new CBCustomer(customerId,firstName,lastName,email); + if (isOneTimeProduct()){ + if (checkProductTypeFiled()) { + if (productType.trim().equalsIgnoreCase(OneTimeProductType.CONSUMABLE.getValue())) { + purchaseNonSubscriptionProduct(OneTimeProductType.CONSUMABLE); + } else if (productType.trim().equalsIgnoreCase(OneTimeProductType.NON_CONSUMABLE.getValue())) { + purchaseNonSubscriptionProduct(OneTimeProductType.NON_CONSUMABLE); + } + dialog.dismiss(); + } + } else { purchaseProduct(customerId); - //purchaseProduct(); + // purchaseProduct(); dialog.dismiss(); } }); dialog.show(); } - private void purchaseProduct(String customerId){ + private boolean checkProductTypeFiled(){ + if (inputProductType.getText().toString().length() == 0) { + inputProductType.setError("This field is required"); + return false; + } + return true; + } + + private boolean isOneTimeProduct(){ + return productList.get(position).getProductType().equalsIgnoreCase(ProductType.INAPP.getValue()); + } + + private void purchaseProduct(String customerId) { + showProgressDialog(); this.billingViewModel.purchaseProduct(this, productList.get(position), customerId); } - private void purchaseProduct(){ - this.billingViewModel.purchaseProduct(this,productList.get(position), cbCustomer); + + private void purchaseProduct() { + showProgressDialog(); + this.billingViewModel.purchaseProduct(this, productList.get(position), cbCustomer); + } + + private void purchaseNonSubscriptionProduct(OneTimeProductType productType) { + showProgressDialog(); + this.billingViewModel.purchaseNonSubscriptionProduct(this, productList.get(position), cbCustomer, productType); } private void updateSubscribeStatus(){ diff --git a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt index 932d568..1017446 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -6,9 +6,7 @@ import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.chargebee.android.Chargebee -import com.chargebee.android.ErrorDetail -import com.chargebee.android.billingservice.CBCallback -import com.chargebee.android.billingservice.CBPurchase +import com.chargebee.android.billingservice.* import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.CBProductIDResult import com.chargebee.android.exceptions.ChargebeeResult @@ -181,4 +179,64 @@ class BillingViewModel : ViewModel() { editor.putString("productId", productId) editor.apply() } + + fun purchaseNonSubscriptionProduct(context: Context,product: CBProduct, customer: CBCustomer, productType: OneTimeProductType) { + // Cache the product id in sharedPreferences and retry validating the receipt if in case server is not responding or no internet connection. + sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) + + CBPurchase.purchaseNonSubscriptionProduct( + product, customer, + productType, object : CBCallback.OneTimePurchaseCallback { + override fun onSuccess(result: NonSubscriptionResponse, status:Boolean) { + Log.i(TAG, "invoice ID: ${result.invoiceId}") + Log.i(TAG, "charge ID: ${result.chargeId}") + productPurchaseResult.postValue(status) + } + override fun onError(error: CBException) { + try { + // Handled server not responding and offline + if (error.httpStatusCode!! in 500..599) { + storeInLocal(product.productId) + validateNonSubscriptionReceipt(context = context, product = product, productType = productType) + } else { + cbException.postValue(error) + } + } catch (exp: Exception) { + Log.i(TAG, "Exception :${exp.message}") + } + } + }) + } + + private fun validateNonSubscriptionReceipt(context: Context, product: CBProduct, productType: OneTimeProductType) { + val customer = CBCustomer( + id = "sync_receipt_android", + firstName = "Test", + lastName = "Purchase", + email = "testreceipt@gmail.com" + ) + CBPurchase.validateReceiptForNonSubscriptions( + context = context, + product = product, + customer = customer, + productType = productType, + completionCallback = object : CBCallback.OneTimePurchaseCallback { + override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + Log.i(TAG, "Invoice ID: ${result.invoiceId}") + Log.i(TAG, "Plan ID: ${result.chargeId}") + // Clear the local cache once receipt validation success + val editor = sharedPreference.edit() + editor.clear().apply() + productPurchaseResult.postValue(status) + } + + override fun onError(error: CBException) { + try { + cbException.postValue(error) + } catch (exp: Exception) { + Log.i(TAG, "Exception :${exp.message}") + } + } + }) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_customer_layout.xml b/app/src/main/res/layout/dialog_customer_layout.xml index 69acf0a..cf511dd 100644 --- a/app/src/main/res/layout/dialog_customer_layout.xml +++ b/app/src/main/res/layout/dialog_customer_layout.xml @@ -74,6 +74,25 @@ android:hint="Email" android:inputType="textPersonName" /> + + + + app:layout_constraintTop_toBottomOf="@+id/productTypeInputLayout" /> \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 67ff6cb..ad911c7 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -2,8 +2,6 @@ package com.chargebee.android import android.text.TextUtils import android.util.Log -import com.chargebee.android.BuildConfig -import com.chargebee.android.billingservice.CBPurchase import com.chargebee.android.exceptions.* import com.chargebee.android.gateway.GatewayTokenizer import com.chargebee.android.loggers.CBLogger diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt index 777600f..0dbc1fa 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -12,6 +12,7 @@ import com.chargebee.android.billingservice.BillingErrorCode.Companion.throwCBEx import com.chargebee.android.models.PurchaseTransaction import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult +import com.chargebee.android.models.CBNonSubscriptionResponse import com.chargebee.android.models.CBProduct import com.chargebee.android.network.CBReceiptResponse import com.chargebee.android.restore.CBRestorePurchaseManager @@ -28,30 +29,55 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private val TAG = javaClass.simpleName lateinit var product: CBProduct private lateinit var restorePurchaseCallBack: CBCallback.RestorePurchaseCallback + private var oneTimePurchaseCallback: CBCallback.OneTimePurchaseCallback? = null init { this.mContext = context } internal fun retrieveProducts( - @BillingClient.SkuType skuType: String, skuList: ArrayList, callBack: CBCallback.ListProductsCallback> + ) { + val productsList = ArrayList() + retrieveProducts(ProductType.SUBS.value, skuList, { subsProductsList -> + productsList.addAll(subsProductsList) + retrieveProducts(ProductType.INAPP.value, skuList, { inAppProductsList -> + productsList.addAll(inAppProductsList) + callBack.onSuccess(productsList) + }, { error -> + callBack.onError(error) + }) + }, { error -> + callBack.onError(error) + }) + } + + internal fun retrieveProducts( + @BillingClient.SkuType skuType: String, + skuList: ArrayList, response: (ArrayList) -> Unit, + errorDetail: (CBException) -> Unit ) { onConnected({ status -> if (status) - loadProductDetails(skuType, skuList, callBack) + loadProductDetails(skuType, skuList, { + response(it) + }, { + errorDetail(it) + }) else - callBack.onError( + errorDetail( connectionError ) }, { error -> - callBack.onError(error) + errorDetail(error) }) } /* Get the SKU/Products from Play Console */ private fun loadProductDetails( @BillingClient.SkuType skuType: String, - skuList: ArrayList, callBack: CBCallback.ListProductsCallback> + skuList: ArrayList, + response: (ArrayList) -> Unit, + errorDetail: (CBException) -> Unit ) { try { val params = SkuDetailsParams @@ -72,14 +98,15 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { skuProduct.title, skuProduct.price, skuProduct, - false + false, + skuProduct.type ) skusWithSkuDetails.add(product) } Log.i(TAG, "Product details :$skusWithSkuDetails") - callBack.onSuccess(productIDs = skusWithSkuDetails) + response(skusWithSkuDetails) } catch (ex: CBException) { - callBack.onError( + errorDetail( CBException( ErrorDetail( message = "Error while parsing data", @@ -91,14 +118,14 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } else { Log.e(TAG, "Response Code :" + billingResult.responseCode) - callBack.onError( + errorDetail( throwCBException(billingResult) ) } } } catch (exp: CBException) { Log.e(TAG, "exception :$exp.message") - callBack.onError(CBException(ErrorDetail(message = "${exp.message}"))) + errorDetail(CBException(ErrorDetail(message = "${exp.message}"))) } } @@ -133,14 +160,21 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { billingResult?.responseCode != OK }?.let { billingResult -> Log.e(TAG, "Failed to launch billing flow $billingResult") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.LaunchBillingFlowError.errorMsg, - httpStatusCode = billingResult.responseCode - ) + val billingError = CBException( + ErrorDetail( + message = GPErrorCode.LaunchBillingFlowError.errorMsg, + httpStatusCode = billingResult.responseCode ) ) + if (product.skuDetails.type == ProductType.SUBS.value) { + purchaseCallBack?.onError( + billingError + ) + } else { + oneTimePurchaseCallback?.onError( + billingError + ) + } } } @@ -219,25 +253,54 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } else -> { - purchaseCallBack?.onError( - throwCBException(billingResult) - ) + if (product.skuDetails.type == ProductType.SUBS.value) + purchaseCallBack?.onError( + throwCBException(billingResult) + ) + else + oneTimePurchaseCallback?.onError( + throwCBException(billingResult) + ) } } } /* Acknowledge the Purchases */ private fun acknowledgePurchase(purchase: Purchase) { + when(product.skuDetails.type){ + ProductType.SUBS.value -> { + isAcknowledgedPurchase(purchase,{ + validateReceipt(purchase.purchaseToken, product) + }, { + purchaseCallBack?.onError(it) + }) + } + ProductType.INAPP.value -> { + if (CBPurchase.productType == OneTimeProductType.CONSUMABLE) { + consumeAsyncPurchase(purchase.purchaseToken) + } else { + isAcknowledgedPurchase(purchase, { + validateNonSubscriptionReceipt(purchase.purchaseToken, product) + }, { + oneTimePurchaseCallback?.onError(it) + }) + } + } + } + } + + private fun isAcknowledgedPurchase(purchase: Purchase, success: () -> Unit, error: (CBException) -> Unit){ if (!purchase.isAcknowledged) { val params = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient?.acknowledgePurchase(params) { billingResult -> - if (billingResult.responseCode == OK) { - try { - if (purchase.purchaseToken.isEmpty()) { - Log.e(TAG, "Receipt Not Found") - purchaseCallBack?.onError( + when (billingResult.responseCode) { + OK -> { + if (purchase.purchaseToken.isNotEmpty()) { + success() + } else { + error( CBException( ErrorDetail( message = GPErrorCode.PurchaseReceiptNotFound.errorMsg, @@ -245,21 +308,52 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) ) ) - } else { - Log.i(TAG, "Google Purchase - success") - Log.i(TAG, "Purchase Token -${purchase.purchaseToken}") - validateReceipt(purchase.purchaseToken, product) } - - } catch (ex: CBException) { - Log.e("Error", ex.toString()) - purchaseCallBack?.onError(CBException(ErrorDetail(message = ex.message))) + } + else -> { + error( + throwCBException(billingResult) + ) } } } } } + /* Consume the Purchases */ + private fun consumeAsyncPurchase(token: String) { + consumePurchase(token) { billingResult, purchaseToken -> + when(billingResult.responseCode){ + OK -> { + validateNonSubscriptionReceipt(purchaseToken, product) + } + else -> { + oneTimePurchaseCallback?.onError( + throwCBException(billingResult) + ) + } + } + } + } + + internal fun consumePurchase( + token: String, + onConsumed: (billingResult: BillingResult, purchaseToken: String) -> Unit + ) { + onConnected({ status -> + if (status) + billingClient?.consumeAsync( + ConsumeParams.newBuilder().setPurchaseToken(token).build(), onConsumed + ) + else + oneTimePurchaseCallback?.onError( + connectionError + ) + }, { error -> + oneTimePurchaseCallback?.onError(error) + }) + } + /* Chargebee method called here to validate receipt */ private fun validateReceipt(purchaseToken: String, product: CBProduct) { try { @@ -324,12 +418,12 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private fun queryPurchaseHistory( storeTransactions: (List) -> Unit ) { - queryAllSubsPurchaseHistory(CBPurchase.ProductType.SUBS.value) { subscriptionHistory -> - queryAllInAppPurchaseHistory(CBPurchase.ProductType.INAPP.value) { inAppHistory -> - val purchaseTransactionHistory = inAppHistory?.let { - subscriptionHistory?.plus(it) - } - storeTransactions(purchaseTransactionHistory ?: emptyList()) + val purchaseTransactionHistory = mutableListOf() + queryAllSubsPurchaseHistory(ProductType.SUBS.value) { subscriptionHistory -> + purchaseTransactionHistory.addAll(subscriptionHistory ?: emptyList()) + queryAllInAppPurchaseHistory(ProductType.INAPP.value) { inAppHistory -> + purchaseTransactionHistory.addAll(inAppHistory ?: emptyList()) + storeTransactions(purchaseTransactionHistory) } } } @@ -435,4 +529,68 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { completionCallback.onError(error) }) } + + internal fun purchaseNonSubscriptionProduct( + product: CBProduct, + oneTimePurchaseCallback: CBCallback.OneTimePurchaseCallback + ) { + this.oneTimePurchaseCallback = oneTimePurchaseCallback + onConnected({ status -> + if (status) + purchase(product) + else + oneTimePurchaseCallback.onError( + connectionError + ) + }, { error -> + oneTimePurchaseCallback.onError(error) + }) + } + + /* Chargebee method called here to validate receipt */ + private fun validateNonSubscriptionReceipt(purchaseToken: String, product: CBProduct) { + CBPurchase.validateNonSubscriptionReceipt(purchaseToken, product) { + when (it) { + is ChargebeeResult.Success -> { + Log.i( + TAG, + "Validate Non-Subscription Receipt Response: ${(it.data as CBNonSubscriptionResponse).nonSubscription}" + ) + if (it.data.nonSubscription != null) { + val invoiceId = (it.data).nonSubscription.invoiceId + Log.i(TAG, "Invoice ID: $invoiceId") + val subscriptionResult = (it.data).nonSubscription + if (invoiceId.isEmpty()) { + oneTimePurchaseCallback?.onSuccess(subscriptionResult, false) + } else { + oneTimePurchaseCallback?.onSuccess(subscriptionResult, true) + } + } else { + oneTimePurchaseCallback?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) + } + } + is ChargebeeResult.Error -> { + Log.e(TAG, "Exception from Server - validateNonSubscriptionReceipt() : ${it.exp.message}") + oneTimePurchaseCallback?.onError(it.exp) + } + } + } + } + + internal fun validateNonSubscriptionReceiptWithChargebee(product: CBProduct, completionCallback: CBCallback.OneTimePurchaseCallback) { + onConnected({ status -> + if (status) + queryPurchaseHistory { purchaseHistoryList -> + val purchaseTransaction = purchaseHistoryList.filter { + it.productId.first() == product.productId + } + validateNonSubscriptionReceipt(purchaseTransaction.first().purchaseToken, product) + } else + completionCallback.onError( + connectionError + ) + }, { error -> + completionCallback.onError(error) + }) + } } diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt index ff78e22..022a818 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt @@ -24,4 +24,9 @@ interface CBCallback { fun onSuccess(result: List) fun onError(error: CBException) } + + interface OneTimePurchaseCallback { + fun onSuccess(result: NonSubscriptionResponse, status: Boolean) + fun onError(error: CBException) + } } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt index 595a477..b08af8b 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -19,11 +19,7 @@ object CBPurchase { val productIdList = arrayListOf() private var customer: CBCustomer? = null internal var includeInActivePurchases = false - - internal enum class ProductType(val value: String) { - SUBS("subs"), - INAPP("inapp") - } + internal lateinit var productType: OneTimeProductType /* * Get the product ID's from chargebee system @@ -54,7 +50,7 @@ object CBPurchase { params: ArrayList, callBack: CBCallback.ListProductsCallback> ) { - sharedInstance(context).retrieveProducts(ProductType.SUBS.value, params, callBack) + sharedInstance(context).retrieveProducts(params, callBack) } /** @@ -90,28 +86,53 @@ object CBPurchase { } private fun purchaseProduct(product: CBProduct, callback: CBCallback.PurchaseCallback) { + isSDKKeyValid({ + billingClientManager?.purchase(product, callback) + }, { + callback.onError(it) + }) + } + + /** + * Buy the non-subscription product with/without customer data + * @param [product] The product that wish to purchase + * @param [customer] Optional. Customer Object. + * @param [productType] One time Product Type. Consumable or Non-Consumable + * @param [callback] listener will be called when product purchase completes. + */ + @JvmStatic + fun purchaseNonSubscriptionProduct( + product: CBProduct, customer: CBCustomer? = null, + productType: OneTimeProductType, + callback: CBCallback.OneTimePurchaseCallback + ) { + this.customer = customer + this.productType = productType + isSDKKeyValid({ + billingClientManager?.purchaseNonSubscriptionProduct(product, callback) + }, { + callback.onError(it) + }) + } + + private fun isSDKKeyValid(success: () -> Unit, error: (CBException) -> Unit) { if (!TextUtils.isEmpty(Chargebee.sdkKey)) { CBAuthentication.isSDKKeyValid(Chargebee.sdkKey) { when (it) { is ChargebeeResult.Success -> { if (billingClientManager?.isFeatureSupported() == true) { - if (billingClientManager?.isBillingClientReady() == true) { - billingClientManager?.purchase(product, callback) - } else { - callback.onError(CBException(ErrorDetail(GPErrorCode.BillingClientNotReady.errorMsg))) - } + success() } else { - callback.onError(CBException(ErrorDetail(GPErrorCode.FeatureNotSupported.errorMsg))) + error(CBException(ErrorDetail(GPErrorCode.FeatureNotSupported.errorMsg, httpStatusCode = BillingErrorCode.FEATURE_NOT_SUPPORTED.code))) } } is ChargebeeResult.Error -> { - Log.i(javaClass.simpleName, "Exception from server :${it.exp.message}") - callback.onError(it.exp) + error(it.exp) } } } } else { - callback.onError( + error( CBException( ErrorDetail( message = GPErrorCode.SDKKeyNotAvailable.errorMsg, @@ -191,7 +212,8 @@ object CBPurchase { purchaseToken, productId, customer, - Chargebee.channel + Chargebee.channel, + null ) ResultHandler.safeExecuter( { ReceiptResource().validateReceipt(params) }, @@ -200,6 +222,57 @@ object CBPurchase { ) } + /** + * This method will be used to validate the receipt with Chargebee, + * when syncing with Chargebee fails after the successful purchase in Google Play Store. + * + * @param [context] Current activity context + * @param [productId] Product Identifier. + * @param [customer] Optional. Customer Object. + * @param [productType] Product Type. Consumable or Non-Consumable product + * @param [completionCallback] The listener will be called when validate receipt completes. + */ + @JvmStatic + fun validateReceiptForNonSubscriptions( + context: Context, + product: CBProduct, + customer: CBCustomer? = null, + productType: OneTimeProductType, + completionCallback: CBCallback.OneTimePurchaseCallback + ) { + this.customer = customer + this.productType = productType + sharedInstance(context).validateNonSubscriptionReceiptWithChargebee(product, completionCallback) + } + + internal fun validateNonSubscriptionReceipt( + purchaseToken: String, + product: CBProduct, + completion: (ChargebeeResult) -> Unit + ) { + validateNonSubscriptionReceipt(purchaseToken, product.productId, completion) + } + + internal fun validateNonSubscriptionReceipt( + purchaseToken: String, + productId: String, + completion: (ChargebeeResult) -> Unit + ) { + val logger = CBLogger(name = "buy", action = "one_time_purchase") + val params = Params( + purchaseToken, + productId, + customer, + Chargebee.channel, + productType + ) + ResultHandler.safeExecuter( + { ReceiptResource().validateReceiptForNonSubscription(params) }, + completion, + logger + ) + } + /* * Get the product ID's from chargebee system. */ diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/OneTimeProductType.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/OneTimeProductType.kt new file mode 100644 index 0000000..d1dc5d8 --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/OneTimeProductType.kt @@ -0,0 +1,7 @@ +package com.chargebee.android.billingservice + +enum class OneTimeProductType(val value: String) { + UNKNOWN(""), + CONSUMABLE("consumable"), + NON_CONSUMABLE("non_consumable") +} \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt new file mode 100644 index 0000000..e4fbc46 --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt @@ -0,0 +1,6 @@ +package com.chargebee.android.billingservice + +enum class ProductType(val value: String) { + SUBS("subs"), + INAPP("inapp") +} \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt b/chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt new file mode 100644 index 0000000..3b3b867 --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt @@ -0,0 +1,17 @@ +package com.chargebee.android.models + +import com.google.gson.annotations.SerializedName + +data class NonSubscriptionResponse( + @SerializedName("invoice_id") + val invoiceId: String, + @SerializedName("customer_id") + val customerId: String, + @SerializedName("charge_id") + val chargeId: String +) + +data class CBNonSubscriptionResponse( + @SerializedName("non_subscription") + val nonSubscription: NonSubscriptionResponse +) diff --git a/chargebee/src/main/java/com/chargebee/android/models/Products.kt b/chargebee/src/main/java/com/chargebee/android/models/Products.kt index 3265a4b..4a4f8e4 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/Products.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/Products.kt @@ -2,5 +2,11 @@ package com.chargebee.android.models import com.android.billingclient.api.SkuDetails -data class CBProduct(val productId: String,val productTitle:String, val productPrice: String, var skuDetails: SkuDetails, var subStatus: Boolean ) { -} \ No newline at end of file +data class CBProduct( + val productId: String, + val productTitle: String, + val productPrice: String, + var skuDetails: SkuDetails, + var subStatus: Boolean, + var productType: String +) \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/network/CBReceiptRequestBody.kt b/chargebee/src/main/java/com/chargebee/android/network/CBReceiptRequestBody.kt index 470b626..c129f3b 100644 --- a/chargebee/src/main/java/com/chargebee/android/network/CBReceiptRequestBody.kt +++ b/chargebee/src/main/java/com/chargebee/android/network/CBReceiptRequestBody.kt @@ -1,16 +1,22 @@ package com.chargebee.android.network -internal class CBReceiptRequestBody( val receipt: String, - val productId: String, - val customer: CBCustomer?, - val channel: String) { +import com.chargebee.android.billingservice.OneTimeProductType + +internal class CBReceiptRequestBody( + val receipt: String, + val productId: String, + val customer: CBCustomer?, + val channel: String, + val productType: OneTimeProductType? +) { companion object { fun fromCBReceiptReqBody(params: Params): CBReceiptRequestBody { return CBReceiptRequestBody( - params.receipt, + params.receipt, params.productId, params.customer, - params.channel + params.channel, + params.productType ) } } @@ -35,6 +41,7 @@ internal class CBReceiptRequestBody( val receipt: String, "channel" to this.channel ) } + fun toMap(): Map { return mapOf( "receipt" to this.receipt, @@ -42,14 +49,38 @@ internal class CBReceiptRequestBody( val receipt: String, "channel" to this.channel ) } + + fun toCBNonSubscriptionReqCustomerBody(): Map { + return mapOf( + "receipt" to this.receipt, + "product[id]" to this.productId, + "customer[id]" to this.customer?.id, + "customer[first_name]" to this.customer?.firstName, + "customer[last_name]" to this.customer?.lastName, + "customer[email]" to this.customer?.email, + "channel" to this.channel, + "product[type]" to this.productType?.value + ) + } + + fun toMapNonSubscription(): Map { + return mapOf( + "receipt" to this.receipt, + "product[id]" to this.productId, + "channel" to this.channel, + "product[type]" to this.productType?.value + ) + } } data class Params( val receipt: String, val productId: String, val customer: CBCustomer?, - val channel: String + val channel: String, + val productType: OneTimeProductType? ) + data class CBCustomer( val id: String?, val firstName: String?, diff --git a/chargebee/src/main/java/com/chargebee/android/repository/ReceiptRepository.kt b/chargebee/src/main/java/com/chargebee/android/repository/ReceiptRepository.kt index 5850a7f..424039a 100644 --- a/chargebee/src/main/java/com/chargebee/android/repository/ReceiptRepository.kt +++ b/chargebee/src/main/java/com/chargebee/android/repository/ReceiptRepository.kt @@ -1,6 +1,7 @@ package com.chargebee.android.repository import com.chargebee.android.Chargebee +import com.chargebee.android.models.CBNonSubscriptionResponse import com.chargebee.android.network.CBReceiptResponse import retrofit2.Response import retrofit2.http.* @@ -15,4 +16,13 @@ interface ReceiptRepository { @Header("version") sdkVersion: String = Chargebee.sdkVersion, @Path("sdkKey") sdkKey: String = Chargebee.sdkKey, @FieldMap data: Map): Response + + @FormUrlEncoded + @POST("v2/non_subscriptions/{sdkKey}/one_time_purchase/") + suspend fun validateReceiptForNonSubscription( + @Header("Authorization") token: String = Chargebee.encodedApiKey, + @Header("platform") platform: String = Chargebee.platform, + @Header("version") sdkVersion: String = Chargebee.sdkVersion, + @Path("sdkKey") sdkKey: String = Chargebee.sdkKey, + @FieldMap data: Map): Response } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/resources/ReceiptResource.kt b/chargebee/src/main/java/com/chargebee/android/resources/ReceiptResource.kt index 07f4921..0210364 100644 --- a/chargebee/src/main/java/com/chargebee/android/resources/ReceiptResource.kt +++ b/chargebee/src/main/java/com/chargebee/android/resources/ReceiptResource.kt @@ -12,9 +12,8 @@ import com.chargebee.android.responseFromServer internal class ReceiptResource : BaseResource(baseUrl = Chargebee.baseUrl){ internal suspend fun validateReceipt(params: Params): ChargebeeResult { - var dataMap = mapOf() val paramDetail = CBReceiptRequestBody.fromCBReceiptReqBody(params) - dataMap = if (params.customer != null && !(TextUtils.isEmpty(params.customer.id))) { + val dataMap = if (params.customer != null && !(TextUtils.isEmpty(params.customer.id))) { paramDetail.toCBReceiptReqCustomerBody() } else{ paramDetail.toMap() @@ -28,4 +27,19 @@ internal class ReceiptResource : BaseResource(baseUrl = Chargebee.baseUrl){ ) } + internal suspend fun validateReceiptForNonSubscription(params: Params): ChargebeeResult { + val paramDetail = CBReceiptRequestBody.fromCBReceiptReqBody(params) + val dataMap = if (params.customer != null && !(TextUtils.isEmpty(params.customer.id))) { + paramDetail.toCBNonSubscriptionReqCustomerBody() + } else{ + paramDetail.toMapNonSubscription() + } + val response = apiClient.create(ReceiptRepository::class.java) + .validateReceiptForNonSubscription(data = dataMap) + + Log.i(javaClass.simpleName, " validateReceiptForNonSubscription Response :$response") + return responseFromServer( + response + ) + } } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt index 1baced3..7b44d60 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -5,6 +5,7 @@ import com.chargebee.android.ErrorDetail import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBPurchase import com.chargebee.android.billingservice.GPErrorCode +import com.chargebee.android.billingservice.ProductType import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.loggers.CBLogger @@ -65,7 +66,10 @@ class CBRestorePurchaseManager { retrieveRestoreSubscription(purchaseToken, { restorePurchases.add(it) when (it.storeStatus) { - StoreStatus.Active.value -> activeTransactions.add(storeTransaction) + StoreStatus.Active.value -> { + activeTransactions.add(storeTransaction) + allTransactions.add(storeTransaction) + } else -> allTransactions.add(storeTransaction) } getRestorePurchases(storeTransactions) @@ -93,12 +97,8 @@ class CBRestorePurchaseManager { val activePurchases = restorePurchases.filter { subscription -> subscription.storeStatus == StoreStatus.Active.value } - val allPurchases = restorePurchases.filter { subscription -> - subscription.storeStatus == StoreStatus.Active.value || subscription.storeStatus == StoreStatus.InTrial.value - || subscription.storeStatus == StoreStatus.Cancelled.value || subscription.storeStatus == StoreStatus.Paused.value - } if (CBPurchase.includeInActivePurchases) { - completionCallback.onSuccess(allPurchases) + completionCallback.onSuccess(restorePurchases) syncPurchaseWithChargebee(allTransactions) } else { completionCallback.onSuccess(activePurchases) @@ -106,14 +106,20 @@ class CBRestorePurchaseManager { } } restorePurchases.clear() + allTransactions.clear() + activeTransactions.clear() } else { fetchStoreSubscriptionStatus(storeTransactions, completionCallback) } } internal fun syncPurchaseWithChargebee(storeTransactions: ArrayList) { - storeTransactions.forEach { productIdList -> - validateReceipt(productIdList.purchaseToken, productIdList.productId.first()) + storeTransactions.forEach { purchaseTransaction -> + if (purchaseTransaction.productType == ProductType.SUBS.value) { + validateReceipt(purchaseTransaction.purchaseToken, purchaseTransaction.productId.first()) + } else { + validateNonSubscriptionReceipt(purchaseTransaction.purchaseToken, purchaseTransaction.productId.first()) + } } } @@ -132,5 +138,21 @@ class CBRestorePurchaseManager { } } } + + internal fun validateNonSubscriptionReceipt(purchaseToken: String, productId: String) { + CBPurchase.validateNonSubscriptionReceipt(purchaseToken, productId) { + when (it) { + is ChargebeeResult.Success -> { + Log.i(javaClass.simpleName, "result : ${it.data}") + } + is ChargebeeResult.Error -> { + Log.e( + javaClass.simpleName, + "Exception from Server - validateNonSubscriptionReceipt() : ${it.exp.message}" + ) + } + } + } + } } } \ No newline at end of file diff --git a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt index b017906..abd6f45 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -53,7 +53,8 @@ class BillingClientManagerTest { "purchaseToken", "product.productId", customer, - Chargebee.channel + Chargebee.channel, + null ) private val receiptDetail = ReceiptDetail("subscriptionId", "customerId", "planId") @@ -86,7 +87,7 @@ class BillingClientManagerTest { val productIdList = arrayListOf("merchant.pro.android", "merchant.premium.android") CoroutineScope(Dispatchers.IO).launch { - val skuType = CBPurchase.ProductType.SUBS + val skuType = ProductType.SUBS Mockito.`when`(mContext?.let { CBPurchase.retrieveProducts( it, @@ -247,7 +248,7 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true) + val products = CBProduct("","","", skuDetails,true, "subs") val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( @@ -279,7 +280,7 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true) + val products = CBProduct("","","", skuDetails,true, "subs") CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( products,"", @@ -322,7 +323,7 @@ class BillingClientManagerTest { ) ) verify(ReceiptResource(), times(1)).validateReceipt(params) - verify(CBReceiptRequestBody("receipt","",null,""), times(1)).toCBReceiptReqBody() + verify(CBReceiptRequestBody("receipt","",null,"", null), times(1)).toCBReceiptReqBody() } } @Test @@ -348,7 +349,7 @@ class BillingClientManagerTest { ) ) verify(ReceiptResource(), times(1)).validateReceipt(params) - verify(CBReceiptRequestBody("receipt","",null,""), times(1)).toCBReceiptReqBody() + verify(CBReceiptRequestBody("receipt","",null,"", null), times(1)).toCBReceiptReqBody() } } @@ -358,7 +359,7 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true) + val products = CBProduct("","","", skuDetails,true, "subs") val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( @@ -390,7 +391,7 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true) + val products = CBProduct("","","", skuDetails,true, "subs") val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( @@ -421,7 +422,7 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true) + val products = CBProduct("","","", skuDetails,true, "subs") CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( products,customer, @@ -463,7 +464,7 @@ class BillingClientManagerTest { ) ) verify(ReceiptResource(), times(1)).validateReceipt(params) - verify(CBReceiptRequestBody("receipt", "", null, ""), times(1)).toCBReceiptReqBody() + verify(CBReceiptRequestBody("receipt", "", null, "", null), times(1)).toCBReceiptReqBody() } } @@ -493,7 +494,7 @@ class BillingClientManagerTest { ) ) verify(ReceiptResource(), times(1)).validateReceipt(params) - verify(CBReceiptRequestBody("receipt", "", null, ""), times(1)).toCBReceiptReqBody() + verify(CBReceiptRequestBody("receipt", "", null, "", null), times(1)).toCBReceiptReqBody() } } } \ No newline at end of file diff --git a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt index 2111dca..83c71f9 100644 --- a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt @@ -4,6 +4,8 @@ import com.android.billingclient.api.* import com.chargebee.android.Chargebee import com.chargebee.android.ErrorDetail import com.chargebee.android.billingservice.CBCallback.RestorePurchaseCallback +import com.chargebee.android.billingservice.OneTimeProductType +import com.chargebee.android.billingservice.ProductType import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.* @@ -155,7 +157,8 @@ class RestorePurchaseTest { purchaseTransaction.first().purchaseToken, purchaseTransaction.first().productId.first(), customer, - Chargebee.channel + Chargebee.channel, + null ) CBRestorePurchaseManager.validateReceipt( params.receipt, @@ -168,7 +171,7 @@ class RestorePurchaseTest { ) ) Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) - Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", null), Mockito.times(1)) .toCBReceiptReqBody() } } @@ -180,7 +183,8 @@ class RestorePurchaseTest { purchaseTransaction.first().purchaseToken, purchaseTransaction.first().productId.first(), customer, - Chargebee.channel + Chargebee.channel, + null ) CoroutineScope(Dispatchers.IO).launch { Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( @@ -189,7 +193,7 @@ class RestorePurchaseTest { ) ) Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) - Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", null), Mockito.times(1)) .toCBReceiptReqBody() } } @@ -201,7 +205,8 @@ class RestorePurchaseTest { purchaseTransaction.first().purchaseToken, purchaseTransaction.first().productId.first(), customer, - Chargebee.channel + Chargebee.channel, + null ) CBRestorePurchaseManager.syncPurchaseWithChargebee(purchaseTransaction) CoroutineScope(Dispatchers.IO).launch { @@ -211,7 +216,7 @@ class RestorePurchaseTest { ) ) Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) - Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", null), Mockito.times(1)) .toCBReceiptReqBody() } } @@ -223,7 +228,8 @@ class RestorePurchaseTest { purchaseTransaction.first().purchaseToken, purchaseTransaction.first().productId.first(), customer, - Chargebee.channel + Chargebee.channel, + null ) CBRestorePurchaseManager.syncPurchaseWithChargebee(purchaseTransaction) CoroutineScope(Dispatchers.IO).launch { @@ -233,7 +239,7 @@ class RestorePurchaseTest { ) ) Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) - Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", null), Mockito.times(1)) .toCBReceiptReqBody() } } From e7a3a8877f7081d98aecaa34fc1c99c2b7d3ac85 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 3 Jul 2023 13:14:33 +0530 Subject: [PATCH 2/9] Implemented retry mechanism for non subscription purchase --- .../chargebee/example/ExampleApplication.kt | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 415c10f..0f12505 100644 --- a/app/src/main/java/com/chargebee/example/ExampleApplication.kt +++ b/app/src/main/java/com/chargebee/example/ExampleApplication.kt @@ -7,8 +7,11 @@ import android.content.SharedPreferences import android.util.Log import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBPurchase +import com.chargebee.android.billingservice.OneTimeProductType +import com.chargebee.android.billingservice.ProductType import com.chargebee.android.exceptions.CBException import com.chargebee.android.models.CBProduct +import com.chargebee.android.models.NonSubscriptionResponse import com.chargebee.android.network.CBCustomer import com.chargebee.android.network.ReceiptDetail import com.chargebee.example.util.NetworkUtil @@ -17,6 +20,12 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { private lateinit var networkUtil: NetworkUtil private lateinit var sharedPreference: SharedPreferences lateinit var mContext: Context + private val customer = CBCustomer( + id = "sync_receipt_android", + firstName = "Test", + lastName = "Purchase", + email = "testreceipt@gmail.com" + ) override fun onCreate() { super.onCreate() @@ -46,7 +55,10 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { productIdList, object : CBCallback.ListProductsCallback> { override fun onSuccess(productIDs: ArrayList) { - validateReceipt(mContext, productIDs.first()) + if (productIDs.first().skuDetails.type == ProductType.SUBS.value) + validateReceipt(mContext, productIDs.first()) + else + validateNonSubscriptionReceipt(mContext, productIDs.first()) } override fun onError(error: CBException) { @@ -56,12 +68,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { } private fun validateReceipt(context: Context, product: CBProduct) { - val customer = CBCustomer( - id = "sync_receipt_android", - firstName = "Test", - lastName = "Purchase", - email = "testreceipt@gmail.com" - ) + CBPurchase.validateReceipt( context = context, product = product, @@ -82,4 +89,27 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { } }) } + + private fun validateNonSubscriptionReceipt(context: Context, product: CBProduct) { + CBPurchase.validateReceiptForNonSubscriptions( + context = context, + product = product, + customer = customer, + productType = OneTimeProductType.CONSUMABLE, + completionCallback = object : CBCallback.OneTimePurchaseCallback { + override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + // Clear the local cache once receipt validation success + val editor = sharedPreference.edit() + editor.clear().apply() + Log.i(javaClass.simpleName, "Subscription ID: ${result.invoiceId}") + Log.i(javaClass.simpleName, "Plan ID: ${result.chargeId}") + Log.i(javaClass.simpleName, "Customer ID: ${result.customerId}") + Log.i(javaClass.simpleName, "Status: $status") + } + + override fun onError(error: CBException) { + Log.e(javaClass.simpleName, "Exception :$error") + } + }) + } } \ No newline at end of file From 04fc1848eed648ee70568686041e5276d4763079 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 4 Jul 2023 13:10:38 +0530 Subject: [PATCH 3/9] Updated productType and Added unit test case for no subscription receipt --- .../chargebee/example/ExampleApplication.kt | 2 +- .../android/billingservice/CBPurchase.kt | 2 +- .../BillingClientManagerTest.kt | 132 +++++++++++++++++- .../android/restore/RestorePurchaseTest.kt | 48 +++++++ 4 files changed, 179 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 0f12505..2aa3d7b 100644 --- a/app/src/main/java/com/chargebee/example/ExampleApplication.kt +++ b/app/src/main/java/com/chargebee/example/ExampleApplication.kt @@ -55,7 +55,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { productIdList, object : CBCallback.ListProductsCallback> { override fun onSuccess(productIDs: ArrayList) { - if (productIDs.first().skuDetails.type == ProductType.SUBS.value) + if (productIDs.first().productType == ProductType.SUBS.value) validateReceipt(mContext, productIDs.first()) else validateNonSubscriptionReceipt(mContext, productIDs.first()) diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt index b08af8b..49bb331 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -19,7 +19,7 @@ object CBPurchase { val productIdList = arrayListOf() private var customer: CBCustomer? = null internal var includeInActivePurchases = false - internal lateinit var productType: OneTimeProductType + internal var productType = OneTimeProductType.UNKNOWN /* * Get the product ID's from chargebee system diff --git a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt index abd6f45..6acfdc9 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -10,6 +10,8 @@ import com.chargebee.android.billingservice.CBCallback.ListProductsCallback import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.CBProductIDResult import com.chargebee.android.exceptions.ChargebeeResult +import com.chargebee.android.models.CBNonSubscriptionResponse +import com.chargebee.android.models.NonSubscriptionResponse import com.chargebee.android.models.CBProduct import com.chargebee.android.network.* import com.chargebee.android.resources.CatalogVersion @@ -57,6 +59,8 @@ class BillingClientManagerTest { null ) private val receiptDetail = ReceiptDetail("subscriptionId", "customerId", "planId") + private var callBackOneTimePurchase: CBCallback.OneTimePurchaseCallback? = null + private val nonSubscriptionDetail = NonSubscriptionResponse("invoiceId", "customerId", "chargeId") @Before fun setUp() { @@ -87,7 +91,6 @@ class BillingClientManagerTest { val productIdList = arrayListOf("merchant.pro.android", "merchant.premium.android") CoroutineScope(Dispatchers.IO).launch { - val skuType = ProductType.SUBS Mockito.`when`(mContext?.let { CBPurchase.retrieveProducts( it, @@ -116,7 +119,6 @@ class BillingClientManagerTest { @Test fun test_retrieveProducts_error(){ val productIdList = arrayListOf("") - CoroutineScope(Dispatchers.IO).launch { Mockito.`when`(mContext?.let { CBPurchase.retrieveProducts( @@ -273,7 +275,6 @@ class BillingClientManagerTest { } } lock.await() - } @Test fun test_purchaseProduct_error(){ @@ -497,4 +498,129 @@ class BillingClientManagerTest { verify(CBReceiptRequestBody("receipt", "", null, "", null), times(1)).toCBReceiptReqBody() } } + + @Test + fun test_purchaseNonSubscriptionProduct_success(){ + val products = CBProduct("","","", SkuDetails(""),true, "inapp") + val lock = CountDownLatch(1) + CoroutineScope(Dispatchers.IO).launch { + CBPurchase.purchaseNonSubscriptionProduct( + product = products, + productType = OneTimeProductType.CONSUMABLE, + callback = object : CBCallback.OneTimePurchaseCallback { + override fun onError(error: CBException) { + lock.countDown() + assertThat(error, instanceOf(CBException::class.java)) + println(" Error : ${error.message} response code: ${error.httpStatusCode}") + } + + override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + lock.countDown() + assertThat(result, instanceOf(NonSubscriptionResponse::class.java)) + } + }) + + Mockito.`when`(callBackOneTimePurchase?.let { + billingClientManager?.purchaseNonSubscriptionProduct(products, it) + }).thenReturn(Unit) + callBackOneTimePurchase?.let { + verify(billingClientManager, times(1))?.purchaseNonSubscriptionProduct( + products, + oneTimePurchaseCallback = it + ) + } + } + lock.await() + } + + @Test + fun test_purchaseNonSubscriptionProduct_error(){ + val products = CBProduct("","","", SkuDetails(""),true, "inapp") + val lock = CountDownLatch(1) + CoroutineScope(Dispatchers.IO).launch { + CBPurchase.purchaseNonSubscriptionProduct( + product = products, + productType = OneTimeProductType.CONSUMABLE, + callback = object : CBCallback.OneTimePurchaseCallback { + override fun onError(error: CBException) { + lock.countDown() + assertThat(error, instanceOf(CBException::class.java)) + } + + override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + lock.countDown() + assertThat(result, instanceOf(NonSubscriptionResponse::class.java)) + } + }) + + Mockito.`when`(callBackOneTimePurchase?.let { + billingClientManager?.purchaseNonSubscriptionProduct(products, it) + }).thenReturn(Unit) + callBackOneTimePurchase?.let { + verify(billingClientManager, times(1))?.purchaseNonSubscriptionProduct( + products, + oneTimePurchaseCallback = it + ) + } + } + lock.await() + } + + @Test + fun test_validateNonSubscriptionReceipt_success(){ + val purchaseToken = "56sadmnagdjsd" + val lock = CountDownLatch(1) + CoroutineScope(Dispatchers.IO).launch { + CBPurchase.validateNonSubscriptionReceipt(purchaseToken, "productId") { + when (it) { + is ChargebeeResult.Success -> { + lock.countDown() + assertThat(it, instanceOf(CBNonSubscriptionResponse::class.java)) + } + is ChargebeeResult.Error -> { + lock.countDown() + println(" Error : ${it.exp.message}") + } + } + } + } + lock.await() + val response = CBNonSubscriptionResponse(nonSubscriptionDetail) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceiptForNonSubscription(it) }).thenReturn( + ChargebeeResult.Success( + response + ) + ) + verify(ReceiptResource(), times(1)).validateReceiptForNonSubscription(params) + verify(CBReceiptRequestBody("receipt","",null,"", null), times(1)).toMapNonSubscription() + } + } + + @Test + fun test_validateNonSubscriptionReceipt_error(){ + val purchaseToken = "56sadmnagdjsd" + CoroutineScope(Dispatchers.IO).launch { + CBPurchase.validateNonSubscriptionReceipt(purchaseToken, "products") { + when (it) { + is ChargebeeResult.Success -> { + assertThat(it, instanceOf(CBNonSubscriptionResponse::class.java)) + } + is ChargebeeResult.Error -> { + println(" Error : ${it.exp.message} response code: ${it.exp.httpStatusCode}") + } + } + } + } + val exception = CBException(ErrorDetail("Error")) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceiptForNonSubscription(it) }).thenReturn( + ChargebeeResult.Error( + exception + ) + ) + verify(ReceiptResource(), times(1)).validateReceiptForNonSubscription(params) + verify(CBReceiptRequestBody("receipt","",null,"", null), times(1)).toMapNonSubscription() + } + } } \ No newline at end of file diff --git a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt index 83c71f9..6f8e483 100644 --- a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt @@ -263,4 +263,52 @@ class RestorePurchaseTest { storeTransactions.add(result) return storeTransactions } + + @Test + fun test_validateNonSubscriptionReceipt_success() { + val purchaseTransaction = getTransaction(true) + val params = Params( + purchaseTransaction.first().purchaseToken, + purchaseTransaction.first().productId.first(), + customer, + Chargebee.channel, + OneTimeProductType.CONSUMABLE + ) + CBRestorePurchaseManager.validateNonSubscriptionReceipt( + params.receipt, + purchaseTransaction.first().productId.first() + ) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceiptForNonSubscription(it) }).thenReturn( + ChargebeeResult.Success( + response + ) + ) + Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceiptForNonSubscription(params) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", OneTimeProductType.CONSUMABLE), Mockito.times(1)) + .toMapNonSubscription() + } + } + + @Test + fun test_validateNonSubscriptionReceipt_failure() { + val purchaseTransaction = getTransaction(false) + val params = Params( + purchaseTransaction.first().purchaseToken, + purchaseTransaction.first().productId.first(), + customer, + Chargebee.channel, + OneTimeProductType.CONSUMABLE + ) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceiptForNonSubscription(it) }).thenReturn( + ChargebeeResult.Error( + error + ) + ) + Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceiptForNonSubscription(params) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", null), Mockito.times(1)) + .toMapNonSubscription() + } + } } \ No newline at end of file From 8ce940694a33b30bd1d47e10ca1d39fc90fc6926 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 5 Jul 2023 11:31:00 +0530 Subject: [PATCH 4/9] Updated test case and removed in-app checks on restore purchase --- .../restore/CBRestorePurchaseManager.kt | 18 ------- .../android/restore/RestorePurchaseTest.kt | 48 ------------------- 2 files changed, 66 deletions(-) diff --git a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt index 7b44d60..6ac3c6f 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -117,8 +117,6 @@ class CBRestorePurchaseManager { storeTransactions.forEach { purchaseTransaction -> if (purchaseTransaction.productType == ProductType.SUBS.value) { validateReceipt(purchaseTransaction.purchaseToken, purchaseTransaction.productId.first()) - } else { - validateNonSubscriptionReceipt(purchaseTransaction.purchaseToken, purchaseTransaction.productId.first()) } } } @@ -138,21 +136,5 @@ class CBRestorePurchaseManager { } } } - - internal fun validateNonSubscriptionReceipt(purchaseToken: String, productId: String) { - CBPurchase.validateNonSubscriptionReceipt(purchaseToken, productId) { - when (it) { - is ChargebeeResult.Success -> { - Log.i(javaClass.simpleName, "result : ${it.data}") - } - is ChargebeeResult.Error -> { - Log.e( - javaClass.simpleName, - "Exception from Server - validateNonSubscriptionReceipt() : ${it.exp.message}" - ) - } - } - } - } } } \ No newline at end of file diff --git a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt index 6f8e483..83c71f9 100644 --- a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt @@ -263,52 +263,4 @@ class RestorePurchaseTest { storeTransactions.add(result) return storeTransactions } - - @Test - fun test_validateNonSubscriptionReceipt_success() { - val purchaseTransaction = getTransaction(true) - val params = Params( - purchaseTransaction.first().purchaseToken, - purchaseTransaction.first().productId.first(), - customer, - Chargebee.channel, - OneTimeProductType.CONSUMABLE - ) - CBRestorePurchaseManager.validateNonSubscriptionReceipt( - params.receipt, - purchaseTransaction.first().productId.first() - ) - CoroutineScope(Dispatchers.IO).launch { - Mockito.`when`(params.let { ReceiptResource().validateReceiptForNonSubscription(it) }).thenReturn( - ChargebeeResult.Success( - response - ) - ) - Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceiptForNonSubscription(params) - Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", OneTimeProductType.CONSUMABLE), Mockito.times(1)) - .toMapNonSubscription() - } - } - - @Test - fun test_validateNonSubscriptionReceipt_failure() { - val purchaseTransaction = getTransaction(false) - val params = Params( - purchaseTransaction.first().purchaseToken, - purchaseTransaction.first().productId.first(), - customer, - Chargebee.channel, - OneTimeProductType.CONSUMABLE - ) - CoroutineScope(Dispatchers.IO).launch { - Mockito.`when`(params.let { ReceiptResource().validateReceiptForNonSubscription(it) }).thenReturn( - ChargebeeResult.Error( - error - ) - ) - Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceiptForNonSubscription(params) - Mockito.verify(CBReceiptRequestBody("receipt", "", null, "", null), Mockito.times(1)) - .toMapNonSubscription() - } - } } \ No newline at end of file From bdc3975b1903a4a12d004f288c7ee07e05642aeb Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 5 Jul 2023 13:25:52 +0530 Subject: [PATCH 5/9] Removed in-app products to restore and updated product type filed in sample app --- .../java/com/chargebee/example/billing/BillingActivity.java | 4 ++-- .../chargebee/android/billingservice/BillingClientManager.kt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java index 8229f02..f8916be 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -128,8 +128,8 @@ private void getCustomerID() { EditText inputLastName = dialog.findViewById(R.id.lastNameText); EditText inputEmail = dialog.findViewById(R.id.emailText); inputProductType = dialog.findViewById(R.id.productTypeText); - if (isOneTimeProduct()) inputProductType.setVisibility(View.GONE); - else inputProductType.setVisibility(View.VISIBLE); + if (isOneTimeProduct()) inputProductType.setVisibility(View.VISIBLE); + else inputProductType.setVisibility(View.GONE); Button dialogButton = dialog.findViewById(R.id.btn_ok); dialogButton.setText("Ok"); diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt index 0dbc1fa..77df4fb 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -298,8 +298,10 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { when (billingResult.responseCode) { OK -> { if (purchase.purchaseToken.isNotEmpty()) { + Log.i(TAG, "Google Purchase - success") success() } else { + Log.e(TAG, "Receipt Not Found") error( CBException( ErrorDetail( @@ -422,7 +424,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { queryAllSubsPurchaseHistory(ProductType.SUBS.value) { subscriptionHistory -> purchaseTransactionHistory.addAll(subscriptionHistory ?: emptyList()) queryAllInAppPurchaseHistory(ProductType.INAPP.value) { inAppHistory -> - purchaseTransactionHistory.addAll(inAppHistory ?: emptyList()) + //purchaseTransactionHistory.addAll(inAppHistory ?: emptyList()) storeTransactions(purchaseTransactionHistory) } } From a462b36aa14c0b4d1d75a2af52dee99f51bc07ab Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 5 Jul 2023 14:11:28 +0530 Subject: [PATCH 6/9] Updated README.md --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 86c0823..a46f218 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,32 @@ CBPurchase.purchaseProduct(product=CBProduct, customer=CBCustomer, object : CBCa ``` The above function will handle the purchase against Google Play Store and send the IAP token for server-side token verification to your Chargebee account. Use the Subscription ID returned by the above function, to check for Subscription status on Chargebee and confirm the access - granted or denied. +### One-Time Purchases +The `purchaseNonSubscriptionProduct` function handles the one-time purchase against Google Play Store and sends the IAP receipt for server-side receipt verification to your Chargebee account. Post verification a Charge corresponding to this one-time purchase will be created in Chargebee. There are two types of one-time purchases `consumable` and `non_consumable`. + +```kotlin +CBPurchase.purchaseNonSubscriptionProduct(product = CBProduct, customer = CBCustomer, productType = OneTimeProductType.CONSUMABLE, callback = object : CBCallback.OneTimePurchaseCallback{ + override fun onSuccess(result: NonSubscriptionResponse, status:Boolean) { + Log.i(TAG, "invoice ID: ${result.invoiceId}") + Log.i(TAG, "charge ID: ${result.chargeId}") + Log.i(TAG, "customer ID: ${result.customerId}") + } + override fun onError(error: CBException) { + Log.e(TAG, "Error: ${error.message}") + } +}) + ``` +The given code defines a function named `purchaseNonSubscriptionProduct` in the CBPurchase class, which takes four input parameters: + +- `product`: An instance of `CBProduct` class, initialized with a `SkuDetails` instance representing the product to be purchased from the Google Play Store. +- `customer`: Optional. An instance of `CBCustomer` class, initialized with the customer's details such as `customerID`, `firstName`, `lastName`, and `email`. +- `productType`: An enum instance of `productType` type, indicating the type of product to be purchased. It can be either .`consumable`, or `non_consumable`. +- `callback`: The `OneTimePurchaseCallback` listener will be invoked when product purchase completes. + +The function is called asynchronously, and it returns a `Result` object with a `success` or `failure` case, which can be handled in the listener. +- If the purchase is successful, the listener will be called with the `success` case, it returns `NonSubscriptionResponse` object. which includes the `customerID`, `chargeID`, and `invoiceID` associated with the purchase. +- If there is any failure during the purchase, the listener will be called with the `error` case, it returns `CBException`. which includes an error object that can be used to handle the error. + ### Restore Purchase The `restorePurchases()` function helps to recover your app user's previous purchases without making them pay again. Sometimes, your app user may want to restore their previous purchases after switching to a new device or reinstalling your app. You can use the `restorePurchases()` function to allow your app user to easily restore their previous purchases. @@ -182,10 +208,10 @@ Receipt validation is crucial to ensure that the purchases made by your users ar * Add a network listener, as shown in the example project. * Save the product identifier in the cache once the purchase is initiated and clear the cache once the purchase is successful. -* When the network connectivity is lost after the purchase is completed at Google Play Store but not synced with Chargebee, retrieve the product from the cache once the network connection is back and initiate validateReceipt() by passing activity `Context`, `CBProduct` and `CBCustomer(optional)` as input. This will validate the receipt and sync the purchase in Chargebee as a subscription. For subscriptions, use the function to validateReceipt(). +* When the network connectivity is lost after the purchase is completed at Google Play Store but not synced with Chargebee, retrieve the product from the cache once the network connection is back and initiate `validateReceipt() / validateReceiptForNonSubscriptions()` by passing activity `Context`, `CBProduct` and `CBCustomer(optional)` as input. This will validate the receipt and sync the purchase in Chargebee as a subscription or one-time purchase. For subscriptions, use the function to `validateReceipt()`;for one-time purchases, use the function `validateReceiptForNonSubscriptions()`. Use the function available for the retry mechanism. -##### Function for validating the receipt +##### Function for validating the Subscriptions receipt ```kotlin CBPurchase.validateReceipt(context = current activity context, product = CBProduct, customer = CBCustomer, object : CBCallback.PurchaseCallback { @@ -200,6 +226,21 @@ CBPurchase.validateReceipt(context = current activity context, product = CBProdu }) ``` +##### Function for validating the One-Time Purchases receipt + +```kotlin +CBPurchase.validateReceiptForNonSubscriptions(context = current activity context, product = CBProduct, customer = CBCustomer, productType = OneTimeProductType.CONSUMABLE, object : CBCallback.OneTimePurchaseCallback { + override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + Log.i(TAG, "invoice ID: ${result.invoiceId}") + Log.i(TAG, "charge ID: ${result.chargeId}") + Log.i(TAG, "customer ID: ${result.customerId}") + } + override fun onError(error: CBException) { + Log.e(TAG, "Error: ${error.message}") + } +}) + ``` + ### Get Subscription Status for Existing Subscribers The following are methods for checking the subscription status of a subscriber who already purchased the product. From 9c019f97d2106dc127c4853d6a3754bc4a5d16dd Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 6 Jul 2023 19:24:24 +0530 Subject: [PATCH 7/9] Address review comments --- README.md | 8 +-- .../chargebee/example/ExampleApplication.kt | 6 +- .../example/billing/BillingActivity.java | 2 +- .../example/billing/BillingViewModel.kt | 4 +- .../billingservice/BillingClientManager.kt | 14 ++--- .../android/billingservice/CBCallback.kt | 2 +- .../android/billingservice/ProductType.kt | 6 +- .../models/CBNonSubscriptionResponse.kt | 4 +- .../com/chargebee/android/models/Products.kt | 3 +- .../BillingClientManagerTest.kt | 56 +++++++++---------- 10 files changed, 53 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index a46f218..2b9f71b 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ The `purchaseNonSubscriptionProduct` function handles the one-time purchase agai ```kotlin CBPurchase.purchaseNonSubscriptionProduct(product = CBProduct, customer = CBCustomer, productType = OneTimeProductType.CONSUMABLE, callback = object : CBCallback.OneTimePurchaseCallback{ - override fun onSuccess(result: NonSubscriptionResponse, status:Boolean) { + override fun onSuccess(result: NonSubscription, status:Boolean) { Log.i(TAG, "invoice ID: ${result.invoiceId}") Log.i(TAG, "charge ID: ${result.chargeId}") Log.i(TAG, "customer ID: ${result.customerId}") @@ -159,12 +159,12 @@ CBPurchase.purchaseNonSubscriptionProduct(product = CBProduct, customer = CBCust The given code defines a function named `purchaseNonSubscriptionProduct` in the CBPurchase class, which takes four input parameters: - `product`: An instance of `CBProduct` class, initialized with a `SkuDetails` instance representing the product to be purchased from the Google Play Store. -- `customer`: Optional. An instance of `CBCustomer` class, initialized with the customer's details such as `customerID`, `firstName`, `lastName`, and `email`. +- `customer`: Optional. An instance of `CBCustomer` class, initialized with the customer's details such as `customerId`, `firstName`, `lastName`, and `email`. - `productType`: An enum instance of `productType` type, indicating the type of product to be purchased. It can be either .`consumable`, or `non_consumable`. - `callback`: The `OneTimePurchaseCallback` listener will be invoked when product purchase completes. The function is called asynchronously, and it returns a `Result` object with a `success` or `failure` case, which can be handled in the listener. -- If the purchase is successful, the listener will be called with the `success` case, it returns `NonSubscriptionResponse` object. which includes the `customerID`, `chargeID`, and `invoiceID` associated with the purchase. +- If the purchase is successful, the listener will be called with the `success` case, it returns `NonSubscriptionResponse` object. which includes the `customerId`, `chargeId`, and `invoiceId` associated with the purchase. - If there is any failure during the purchase, the listener will be called with the `error` case, it returns `CBException`. which includes an error object that can be used to handle the error. ### Restore Purchase @@ -230,7 +230,7 @@ CBPurchase.validateReceipt(context = current activity context, product = CBProdu ```kotlin CBPurchase.validateReceiptForNonSubscriptions(context = current activity context, product = CBProduct, customer = CBCustomer, productType = OneTimeProductType.CONSUMABLE, object : CBCallback.OneTimePurchaseCallback { - override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + override fun onSuccess(result: NonSubscription, status: Boolean) { Log.i(TAG, "invoice ID: ${result.invoiceId}") Log.i(TAG, "charge ID: ${result.chargeId}") Log.i(TAG, "customer ID: ${result.customerId}") diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 2aa3d7b..5e7a473 100644 --- a/app/src/main/java/com/chargebee/example/ExampleApplication.kt +++ b/app/src/main/java/com/chargebee/example/ExampleApplication.kt @@ -11,7 +11,7 @@ import com.chargebee.android.billingservice.OneTimeProductType import com.chargebee.android.billingservice.ProductType import com.chargebee.android.exceptions.CBException import com.chargebee.android.models.CBProduct -import com.chargebee.android.models.NonSubscriptionResponse +import com.chargebee.android.models.NonSubscription import com.chargebee.android.network.CBCustomer import com.chargebee.android.network.ReceiptDetail import com.chargebee.example.util.NetworkUtil @@ -55,7 +55,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { productIdList, object : CBCallback.ListProductsCallback> { override fun onSuccess(productIDs: ArrayList) { - if (productIDs.first().productType == ProductType.SUBS.value) + if (productIDs.first().productType == ProductType.SUBS) validateReceipt(mContext, productIDs.first()) else validateNonSubscriptionReceipt(mContext, productIDs.first()) @@ -97,7 +97,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { customer = customer, productType = OneTimeProductType.CONSUMABLE, completionCallback = object : CBCallback.OneTimePurchaseCallback { - override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + override fun onSuccess(result: NonSubscription, status: Boolean) { // Clear the local cache once receipt validation success val editor = sharedPreference.edit() editor.clear().apply() diff --git a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java index f8916be..753151b 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -167,7 +167,7 @@ private boolean checkProductTypeFiled(){ } private boolean isOneTimeProduct(){ - return productList.get(position).getProductType().equalsIgnoreCase(ProductType.INAPP.getValue()); + return productList.get(position).getProductType() == ProductType.INAPP; } private void purchaseProduct(String customerId) { diff --git a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt index 1017446..3397ace 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -187,7 +187,7 @@ class BillingViewModel : ViewModel() { CBPurchase.purchaseNonSubscriptionProduct( product, customer, productType, object : CBCallback.OneTimePurchaseCallback { - override fun onSuccess(result: NonSubscriptionResponse, status:Boolean) { + override fun onSuccess(result: NonSubscription, status:Boolean) { Log.i(TAG, "invoice ID: ${result.invoiceId}") Log.i(TAG, "charge ID: ${result.chargeId}") productPurchaseResult.postValue(status) @@ -221,7 +221,7 @@ class BillingViewModel : ViewModel() { customer = customer, productType = productType, completionCallback = object : CBCallback.OneTimePurchaseCallback { - override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + override fun onSuccess(result: NonSubscription, status: Boolean) { Log.i(TAG, "Invoice ID: ${result.invoiceId}") Log.i(TAG, "Plan ID: ${result.chargeId}") // Clear the local cache once receipt validation success diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt index 77df4fb..032d1fd 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -99,7 +99,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { skuProduct.price, skuProduct, false, - skuProduct.type + ProductType.getProductType(skuProduct.type) ) skusWithSkuDetails.add(product) } @@ -267,15 +267,15 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { /* Acknowledge the Purchases */ private fun acknowledgePurchase(purchase: Purchase) { - when(product.skuDetails.type){ - ProductType.SUBS.value -> { + when(product.productType){ + ProductType.SUBS -> { isAcknowledgedPurchase(purchase,{ validateReceipt(purchase.purchaseToken, product) }, { purchaseCallBack?.onError(it) }) } - ProductType.INAPP.value -> { + ProductType.INAPP -> { if (CBPurchase.productType == OneTimeProductType.CONSUMABLE) { consumeAsyncPurchase(purchase.purchaseToken) } else { @@ -561,11 +561,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { if (it.data.nonSubscription != null) { val invoiceId = (it.data).nonSubscription.invoiceId Log.i(TAG, "Invoice ID: $invoiceId") - val subscriptionResult = (it.data).nonSubscription + val nonSubscriptionResult = (it.data).nonSubscription if (invoiceId.isEmpty()) { - oneTimePurchaseCallback?.onSuccess(subscriptionResult, false) + oneTimePurchaseCallback?.onSuccess(nonSubscriptionResult, false) } else { - oneTimePurchaseCallback?.onSuccess(subscriptionResult, true) + oneTimePurchaseCallback?.onSuccess(nonSubscriptionResult, true) } } else { oneTimePurchaseCallback?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt index 022a818..7dc8b58 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt @@ -26,7 +26,7 @@ interface CBCallback { } interface OneTimePurchaseCallback { - fun onSuccess(result: NonSubscriptionResponse, status: Boolean) + fun onSuccess(result: NonSubscription, status: Boolean) fun onError(error: CBException) } } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt index e4fbc46..78143aa 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt @@ -2,5 +2,9 @@ package com.chargebee.android.billingservice enum class ProductType(val value: String) { SUBS("subs"), - INAPP("inapp") + INAPP("inapp"); + + companion object { + fun getProductType(value: String): ProductType = ProductType.valueOf(value) + } } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt b/chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt index 3b3b867..96b7da9 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/CBNonSubscriptionResponse.kt @@ -2,7 +2,7 @@ package com.chargebee.android.models import com.google.gson.annotations.SerializedName -data class NonSubscriptionResponse( +data class NonSubscription( @SerializedName("invoice_id") val invoiceId: String, @SerializedName("customer_id") @@ -13,5 +13,5 @@ data class NonSubscriptionResponse( data class CBNonSubscriptionResponse( @SerializedName("non_subscription") - val nonSubscription: NonSubscriptionResponse + val nonSubscription: NonSubscription ) diff --git a/chargebee/src/main/java/com/chargebee/android/models/Products.kt b/chargebee/src/main/java/com/chargebee/android/models/Products.kt index 4a4f8e4..88e63a8 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/Products.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/Products.kt @@ -1,6 +1,7 @@ package com.chargebee.android.models import com.android.billingclient.api.SkuDetails +import com.chargebee.android.billingservice.ProductType data class CBProduct( val productId: String, @@ -8,5 +9,5 @@ data class CBProduct( val productPrice: String, var skuDetails: SkuDetails, var subStatus: Boolean, - var productType: String + var productType: ProductType ) \ No newline at end of file diff --git a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt index 6acfdc9..1511cbe 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -11,8 +11,8 @@ import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.CBProductIDResult import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.CBNonSubscriptionResponse -import com.chargebee.android.models.NonSubscriptionResponse import com.chargebee.android.models.CBProduct +import com.chargebee.android.models.NonSubscription import com.chargebee.android.network.* import com.chargebee.android.resources.CatalogVersion import com.chargebee.android.resources.ReceiptResource @@ -60,7 +60,9 @@ class BillingClientManagerTest { ) private val receiptDetail = ReceiptDetail("subscriptionId", "customerId", "planId") private var callBackOneTimePurchase: CBCallback.OneTimePurchaseCallback? = null - private val nonSubscriptionDetail = NonSubscriptionResponse("invoiceId", "customerId", "chargeId") + private val nonSubscriptionDetail = NonSubscription("invoiceId", "customerId", "chargeId") + private val otpProducts = CBProduct("test.consumable","Example product","100.0", SkuDetails(""),true, productType = ProductType.INAPP) + private val subProducts = CBProduct("chargebee.premium.android","Premium Plan","", SkuDetails(""),true, ProductType.SUBS) @Before fun setUp() { @@ -250,11 +252,11 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true, "subs") + val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,"", + subProducts,"", object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -268,10 +270,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(products, it) + billingClientManager?.purchase(subProducts, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(products,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) } } lock.await() @@ -281,10 +283,9 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true, "subs") CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,"", + subProducts,"", object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { @@ -360,11 +361,10 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true, "subs") val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,customer, + subProducts,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -378,10 +378,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(products, it) + billingClientManager?.purchase(subProducts, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(products,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) } } lock.await() @@ -392,11 +392,10 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true, "subs") val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,customer, + subProducts,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -410,10 +409,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(products, it) + billingClientManager?.purchase(subProducts, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(products,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) } } lock.await() @@ -423,10 +422,9 @@ class BillingClientManagerTest { val jsonDetails = "{\"productId\":\"merchant.premium.android\",\"type\":\"subs\",\"title\":\"Premium Plan (Chargebee Example)\",\"name\":\"Premium Plan\",\"price\":\"₹2,650.00\",\"price_amount_micros\":2650000000,\"price_currency_code\":\"INR\",\"description\":\"Every 6 Months\",\"subscriptionPeriod\":\"P6M\",\"skuDetailsToken\":\"AEuhp4J0KiD1Bsj3Yq2mHPBRNHUBdzs4nTJY3PWRR8neE-22MJNssuDzH2VLFKv35Ov8\"}" val skuDetails = SkuDetails(jsonDetails) - val products = CBProduct("","","", skuDetails,true, "subs") CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,customer, + subProducts,customer, object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { @@ -501,11 +499,10 @@ class BillingClientManagerTest { @Test fun test_purchaseNonSubscriptionProduct_success(){ - val products = CBProduct("","","", SkuDetails(""),true, "inapp") val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseNonSubscriptionProduct( - product = products, + product = otpProducts, productType = OneTimeProductType.CONSUMABLE, callback = object : CBCallback.OneTimePurchaseCallback { override fun onError(error: CBException) { @@ -514,18 +511,18 @@ class BillingClientManagerTest { println(" Error : ${error.message} response code: ${error.httpStatusCode}") } - override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + override fun onSuccess(result: NonSubscription, status: Boolean) { lock.countDown() - assertThat(result, instanceOf(NonSubscriptionResponse::class.java)) + assertThat(result, instanceOf(NonSubscription::class.java)) } }) Mockito.`when`(callBackOneTimePurchase?.let { - billingClientManager?.purchaseNonSubscriptionProduct(products, it) + billingClientManager?.purchaseNonSubscriptionProduct(otpProducts, it) }).thenReturn(Unit) callBackOneTimePurchase?.let { verify(billingClientManager, times(1))?.purchaseNonSubscriptionProduct( - products, + otpProducts, oneTimePurchaseCallback = it ) } @@ -535,11 +532,10 @@ class BillingClientManagerTest { @Test fun test_purchaseNonSubscriptionProduct_error(){ - val products = CBProduct("","","", SkuDetails(""),true, "inapp") val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseNonSubscriptionProduct( - product = products, + product = otpProducts, productType = OneTimeProductType.CONSUMABLE, callback = object : CBCallback.OneTimePurchaseCallback { override fun onError(error: CBException) { @@ -547,18 +543,18 @@ class BillingClientManagerTest { assertThat(error, instanceOf(CBException::class.java)) } - override fun onSuccess(result: NonSubscriptionResponse, status: Boolean) { + override fun onSuccess(result: NonSubscription, status: Boolean) { lock.countDown() - assertThat(result, instanceOf(NonSubscriptionResponse::class.java)) + assertThat(result, instanceOf(NonSubscription::class.java)) } }) Mockito.`when`(callBackOneTimePurchase?.let { - billingClientManager?.purchaseNonSubscriptionProduct(products, it) + billingClientManager?.purchaseNonSubscriptionProduct(otpProducts, it) }).thenReturn(Unit) callBackOneTimePurchase?.let { verify(billingClientManager, times(1))?.purchaseNonSubscriptionProduct( - products, + otpProducts, oneTimePurchaseCallback = it ) } From f1fa065647b6b5c8fdfc36539feffcafa9e7b859 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Fri, 7 Jul 2023 16:54:02 +0530 Subject: [PATCH 8/9] Addressed the review comments and improvements --- .../com/chargebee/example/MainActivity.kt | 39 +++++-------------- .../example/billing/BillingViewModel.kt | 19 +++++++++ .../billingservice/BillingClientManager.kt | 7 +++- .../restore/CBRestorePurchaseManager.kt | 22 ++++++----- 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index d98d565..1987cfd 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -73,6 +73,15 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { Log.i(javaClass.simpleName, "Google play product identifiers: $it") alertListProductId(it) } + + this.mBillingViewModel!!.restorePurchaseResult.observeForever { + hideProgressDialog() + if (it.isNotEmpty()) { + alertSuccess("${it.size} purchases restored successfully") + } else { + alertSuccess("Purchases not found to restore") + } + } } private fun setListAdapter() { @@ -133,7 +142,7 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { getSubscriptionId() } CBMenu.RestorePurchase.value -> { - restorePurchases() + mBillingViewModel?.restorePurchases(this) } else -> { Log.i(javaClass.simpleName, " Not implemented") @@ -208,34 +217,6 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { }) } - private fun restorePurchases() { - showProgressDialog() - CBPurchase.restorePurchases( - context = this, includeInActivePurchases = true, - completionCallback = object : CBCallback.RestorePurchaseCallback { - override fun onSuccess(result: List) { - hideProgressDialog() - result.forEach { - Log.i(javaClass.simpleName, "status : ${it.storeStatus}") - } - CoroutineScope(Dispatchers.Main).launch { - if (result.isNotEmpty()) - alertSuccess("${result.size} purchases restored successfully") - else - alertSuccess("Purchases not found to restore") - } - } - - override fun onError(error: CBException) { - hideProgressDialog() - Log.e(javaClass.simpleName, "error message: ${error.message}") - CoroutineScope(Dispatchers.Main).launch { - showDialog("${error.message}, ${error.httpStatusCode}") - } - } - }) - } - private fun alertListProductId(list: Array) { val builder = AlertDialog.Builder(this) builder.setTitle("Chargebee Product IDs") diff --git a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt index 3397ace..a72e43d 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -26,6 +26,7 @@ class BillingViewModel : ViewModel() { var entitlementsResult: MutableLiveData = MutableLiveData() private var subscriptionId: String = "" private lateinit var sharedPreference : SharedPreferences + var restorePurchaseResult: MutableLiveData> = MutableLiveData() fun purchaseProduct(context: Context,product: CBProduct, customer: CBCustomer) { // Cache the product id in sharedPreferences and retry validating the receipt if in case server is not responding or no internet connection. @@ -239,4 +240,22 @@ class BillingViewModel : ViewModel() { } }) } + + fun restorePurchases(context: Context, includeInActivePurchases: Boolean = false) { + CBPurchase.restorePurchases( + context = context, includeInActivePurchases = includeInActivePurchases, + completionCallback = object : CBCallback.RestorePurchaseCallback { + override fun onSuccess(result: List) { + result.forEach { + Log.i(javaClass.simpleName, "status : ${it.storeStatus}") + Log.i(javaClass.simpleName, "data : $it") + } + restorePurchaseResult.postValue(result) + } + + override fun onError(error: CBException) { + cbException.postValue(error) + } + }) + } } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt index 032d1fd..836cf03 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -406,8 +406,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { val storeTransactions = arrayListOf() storeTransactions.addAll(purchaseHistoryList) CBRestorePurchaseManager.fetchStoreSubscriptionStatus( - storeTransactions, - restorePurchaseCallBack + storeTransactions = storeTransactions, + allTransactions = arrayListOf(), + activeTransactions = arrayListOf(), + restorePurchases = arrayListOf(), + completionCallback = restorePurchaseCallBack ) } } else { diff --git a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt index 6ac3c6f..6cb8c83 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -16,9 +16,6 @@ import com.chargebee.android.resources.RestorePurchaseResource class CBRestorePurchaseManager { companion object { - private var allTransactions = ArrayList() - private var restorePurchases = ArrayList() - private var activeTransactions = ArrayList() private lateinit var completionCallback: CBCallback.RestorePurchaseCallback private fun retrieveStoreSubscription( @@ -56,6 +53,9 @@ class CBRestorePurchaseManager { internal fun fetchStoreSubscriptionStatus( storeTransactions: ArrayList, + allTransactions: ArrayList, + activeTransactions: ArrayList, + restorePurchases: ArrayList, completionCallback: CBCallback.RestorePurchaseCallback ) { this.completionCallback = completionCallback @@ -72,9 +72,9 @@ class CBRestorePurchaseManager { } else -> allTransactions.add(storeTransaction) } - getRestorePurchases(storeTransactions) + getRestorePurchases(storeTransactions, allTransactions, activeTransactions, restorePurchases) }, { _ -> - getRestorePurchases(storeTransactions) + getRestorePurchases(storeTransactions, allTransactions, activeTransactions, restorePurchases) }) } } else { @@ -82,7 +82,12 @@ class CBRestorePurchaseManager { } } - internal fun getRestorePurchases(storeTransactions: ArrayList) { + internal fun getRestorePurchases( + storeTransactions: ArrayList, + allTransactions: ArrayList, + activeTransactions: ArrayList, + restorePurchases: ArrayList + ) { if (storeTransactions.isEmpty()) { if (restorePurchases.isEmpty()) { completionCallback.onError( @@ -105,11 +110,8 @@ class CBRestorePurchaseManager { syncPurchaseWithChargebee(activeTransactions) } } - restorePurchases.clear() - allTransactions.clear() - activeTransactions.clear() } else { - fetchStoreSubscriptionStatus(storeTransactions, completionCallback) + fetchStoreSubscriptionStatus(storeTransactions,allTransactions, activeTransactions,restorePurchases, completionCallback) } } From 9d163426569c6487175dcf3868b35d0b76739722 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 10 Jul 2023 18:52:30 +0530 Subject: [PATCH 9/9] Addressed the review comments and improvements --- .../android/billingservice/BillingClientManager.kt | 6 +++--- .../com/chargebee/android/billingservice/ProductType.kt | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt index 836cf03..b827ac7 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -402,9 +402,9 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { connectionStatus: Boolean ) { if (connectionStatus) { - queryPurchaseHistory { purchaseHistoryList -> + queryAllSubsPurchaseHistory(ProductType.SUBS.value) { purchaseHistoryList -> val storeTransactions = arrayListOf() - storeTransactions.addAll(purchaseHistoryList) + storeTransactions.addAll(purchaseHistoryList ?: emptyList()) CBRestorePurchaseManager.fetchStoreSubscriptionStatus( storeTransactions = storeTransactions, allTransactions = arrayListOf(), @@ -427,7 +427,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { queryAllSubsPurchaseHistory(ProductType.SUBS.value) { subscriptionHistory -> purchaseTransactionHistory.addAll(subscriptionHistory ?: emptyList()) queryAllInAppPurchaseHistory(ProductType.INAPP.value) { inAppHistory -> - //purchaseTransactionHistory.addAll(inAppHistory ?: emptyList()) + purchaseTransactionHistory.addAll(inAppHistory ?: emptyList()) storeTransactions(purchaseTransactionHistory) } } diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt index 78143aa..91064af 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt @@ -1,10 +1,12 @@ package com.chargebee.android.billingservice +import java.util.* + enum class ProductType(val value: String) { SUBS("subs"), INAPP("inapp"); companion object { - fun getProductType(value: String): ProductType = ProductType.valueOf(value) + fun getProductType(value: String): ProductType = ProductType.valueOf(value.toUpperCase(Locale.ROOT)) } } \ No newline at end of file