diff --git a/README.md b/README.md index 8e6e0be..e116c00 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: NonSubscription, 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: NonSubscription, 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. diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 415c10f..5e7a473 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.NonSubscription 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().productType == ProductType.SUBS) + 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: NonSubscription, 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 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/BillingActivity.java b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java index a8fda6d..753151b 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.VISIBLE); + else inputProductType.setVisibility(View.GONE); 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() == ProductType.INAPP; + } + + 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..a72e43d 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 @@ -28,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. @@ -181,4 +180,82 @@ 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: NonSubscription, 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: 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 + 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}") + } + } + }) + } + + 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/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..b827ac7 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, + ProductType.getProductType(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,56 @@ 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.productType){ + ProductType.SUBS -> { + isAcknowledgedPurchase(purchase,{ + validateReceipt(purchase.purchaseToken, product) + }, { + purchaseCallBack?.onError(it) + }) + } + ProductType.INAPP -> { + 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()) { + when (billingResult.responseCode) { + OK -> { + if (purchase.purchaseToken.isNotEmpty()) { + Log.i(TAG, "Google Purchase - success") + success() + } else { Log.e(TAG, "Receipt Not Found") - purchaseCallBack?.onError( + error( CBException( ErrorDetail( message = GPErrorCode.PurchaseReceiptNotFound.errorMsg, @@ -245,21 +310,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 { @@ -306,12 +402,15 @@ 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, - restorePurchaseCallBack + storeTransactions = storeTransactions, + allTransactions = arrayListOf(), + activeTransactions = arrayListOf(), + restorePurchases = arrayListOf(), + completionCallback = restorePurchaseCallBack ) } } else { @@ -324,12 +423,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 +534,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 nonSubscriptionResult = (it.data).nonSubscription + if (invoiceId.isEmpty()) { + oneTimePurchaseCallback?.onSuccess(nonSubscriptionResult, false) + } else { + oneTimePurchaseCallback?.onSuccess(nonSubscriptionResult, 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..7dc8b58 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: NonSubscription, 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..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,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 var productType = OneTimeProductType.UNKNOWN /* * 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..91064af --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/ProductType.kt @@ -0,0 +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.toUpperCase(Locale.ROOT)) + } +} \ 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..96b7da9 --- /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 NonSubscription( + @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: 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 3265a4b..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,13 @@ package com.chargebee.android.models import com.android.billingclient.api.SkuDetails +import com.chargebee.android.billingservice.ProductType -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: ProductType +) \ 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..6cb8c83 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 @@ -15,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( @@ -55,6 +53,9 @@ class CBRestorePurchaseManager { internal fun fetchStoreSubscriptionStatus( storeTransactions: ArrayList, + allTransactions: ArrayList, + activeTransactions: ArrayList, + restorePurchases: ArrayList, completionCallback: CBCallback.RestorePurchaseCallback ) { this.completionCallback = completionCallback @@ -65,12 +66,15 @@ 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) + getRestorePurchases(storeTransactions, allTransactions, activeTransactions, restorePurchases) }, { _ -> - getRestorePurchases(storeTransactions) + getRestorePurchases(storeTransactions, allTransactions, activeTransactions, restorePurchases) }) } } else { @@ -78,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( @@ -93,27 +102,24 @@ 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) syncPurchaseWithChargebee(activeTransactions) } } - restorePurchases.clear() } else { - fetchStoreSubscriptionStatus(storeTransactions, completionCallback) + fetchStoreSubscriptionStatus(storeTransactions,allTransactions, activeTransactions,restorePurchases, 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()) + } } } 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..1511cbe 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -10,7 +10,9 @@ 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.CBProduct +import com.chargebee.android.models.NonSubscription import com.chargebee.android.network.* import com.chargebee.android.resources.CatalogVersion import com.chargebee.android.resources.ReceiptResource @@ -53,9 +55,14 @@ class BillingClientManagerTest { "purchaseToken", "product.productId", customer, - Chargebee.channel + Chargebee.channel, + null ) private val receiptDetail = ReceiptDetail("subscriptionId", "customerId", "planId") + private var callBackOneTimePurchase: CBCallback.OneTimePurchaseCallback? = null + 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() { @@ -86,7 +93,6 @@ class BillingClientManagerTest { val productIdList = arrayListOf("merchant.pro.android", "merchant.premium.android") CoroutineScope(Dispatchers.IO).launch { - val skuType = CBPurchase.ProductType.SUBS Mockito.`when`(mContext?.let { CBPurchase.retrieveProducts( it, @@ -115,7 +121,6 @@ class BillingClientManagerTest { @Test fun test_retrieveProducts_error(){ val productIdList = arrayListOf("") - CoroutineScope(Dispatchers.IO).launch { Mockito.`when`(mContext?.let { CBPurchase.retrieveProducts( @@ -247,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) + val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,"", + subProducts,"", object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -265,24 +270,22 @@ 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() - } @Test fun test_purchaseProduct_error(){ 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) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,"", + subProducts,"", object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { @@ -322,7 +325,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 +351,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,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) val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,customer, + subProducts,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -376,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() @@ -390,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) val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,customer, + subProducts,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -408,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() @@ -421,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) CoroutineScope(Dispatchers.IO).launch { CBPurchase.purchaseProduct( - products,customer, + subProducts,customer, object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { @@ -463,7 +463,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 +493,130 @@ class BillingClientManagerTest { ) ) verify(ReceiptResource(), times(1)).validateReceipt(params) - verify(CBReceiptRequestBody("receipt", "", null, ""), times(1)).toCBReceiptReqBody() + verify(CBReceiptRequestBody("receipt", "", null, "", null), times(1)).toCBReceiptReqBody() + } + } + + @Test + fun test_purchaseNonSubscriptionProduct_success(){ + val lock = CountDownLatch(1) + CoroutineScope(Dispatchers.IO).launch { + CBPurchase.purchaseNonSubscriptionProduct( + product = otpProducts, + 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: NonSubscription, status: Boolean) { + lock.countDown() + assertThat(result, instanceOf(NonSubscription::class.java)) + } + }) + + Mockito.`when`(callBackOneTimePurchase?.let { + billingClientManager?.purchaseNonSubscriptionProduct(otpProducts, it) + }).thenReturn(Unit) + callBackOneTimePurchase?.let { + verify(billingClientManager, times(1))?.purchaseNonSubscriptionProduct( + otpProducts, + oneTimePurchaseCallback = it + ) + } + } + lock.await() + } + + @Test + fun test_purchaseNonSubscriptionProduct_error(){ + val lock = CountDownLatch(1) + CoroutineScope(Dispatchers.IO).launch { + CBPurchase.purchaseNonSubscriptionProduct( + product = otpProducts, + productType = OneTimeProductType.CONSUMABLE, + callback = object : CBCallback.OneTimePurchaseCallback { + override fun onError(error: CBException) { + lock.countDown() + assertThat(error, instanceOf(CBException::class.java)) + } + + override fun onSuccess(result: NonSubscription, status: Boolean) { + lock.countDown() + assertThat(result, instanceOf(NonSubscription::class.java)) + } + }) + + Mockito.`when`(callBackOneTimePurchase?.let { + billingClientManager?.purchaseNonSubscriptionProduct(otpProducts, it) + }).thenReturn(Unit) + callBackOneTimePurchase?.let { + verify(billingClientManager, times(1))?.purchaseNonSubscriptionProduct( + otpProducts, + 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 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() } }