From 6abcbb49a5b995119ba366afd9cc5e737b39fca6 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 1 May 2023 10:13:46 +0530 Subject: [PATCH 01/84] Feature: restore purchases related changes and added new classes --- .../com/chargebee/example/MainActivity.kt | 46 +- .../java/com/chargebee/example/util/CBMenu.kt | 3 +- .../billingservice/BillingClientManager.kt | 478 ++++++++++++------ .../android/billingservice/CBCallback.kt | 6 +- .../android/billingservice/CBPurchase.kt | 80 ++- .../android/billingservice/GPErrorCode.kt | 60 +++ .../android/models/CBRestoreSubscription.kt | 10 + .../android/models/PurchaseTransaction.kt | 8 + .../android/repository/PurchaseRepository.kt | 13 +- .../resources/RestorePurchaseResource.kt | 24 + .../restore/CBRestorePurchaseManager.kt | 134 +++++ 11 files changed, 657 insertions(+), 205 deletions(-) create mode 100644 chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt create mode 100644 chargebee/src/main/java/com/chargebee/android/models/PurchaseTransaction.kt create mode 100644 chargebee/src/main/java/com/chargebee/android/resources/RestorePurchaseResource.kt create mode 100644 chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index d9236e2..576c299 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -16,8 +16,9 @@ import androidx.recyclerview.widget.RecyclerView import com.chargebee.android.Chargebee import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBPurchase +import com.chargebee.android.billingservice.RestorePurchaseCallback +import com.chargebee.android.models.CBRestoreSubscription import com.chargebee.android.exceptions.CBException -import com.chargebee.android.exceptions.CBProductIDResult import com.chargebee.android.models.CBProduct import com.chargebee.example.adapter.ListItemsAdapter import com.chargebee.example.addon.AddonActivity @@ -43,7 +44,7 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { var featureList = mutableListOf() var mContext: Context? = null private val gson = Gson() - private var mBillingViewModel : BillingViewModel? = null + private var mBillingViewModel: BillingViewModel? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -75,7 +76,7 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { } } - private fun setListAdapter(){ + private fun setListAdapter() { featureList = CBMenu.values().toMutableList() listItemsAdapter = ListItemsAdapter(featureList, this) val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(applicationContext) @@ -85,7 +86,7 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { } override fun onItemClick(view: View?, position: Int) { - when(CBMenu.valueOf(featureList.get(position).toString()).value){ + when (CBMenu.valueOf(featureList.get(position).toString()).value) { CBMenu.Configure.value -> { if (view != null) { onClickConfigure(view) @@ -132,8 +133,11 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { CBMenu.GetEntitlements.value -> { getSubscriptionId() } - else ->{ - Log.i(javaClass.simpleName, " Not implemented" ) + CBMenu.RestorePurchase.value -> { + restorePurchases() + } + else -> { + Log.i(javaClass.simpleName, " Not implemented") } } } @@ -148,9 +152,9 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { val builder = AlertDialog.Builder(this) val inflater = layoutInflater val dialogLayout = inflater.inflate(R.layout.activity_configure, null) - val siteNameEditText = dialogLayout.findViewById(R.id.etv_siteName) - val apiKeyEditText = dialogLayout.findViewById(R.id.etv_apikey) - val sdkKeyEditText = dialogLayout.findViewById(R.id.etv_sdkkey) + val siteNameEditText = dialogLayout.findViewById(R.id.etv_siteName) + val apiKeyEditText = dialogLayout.findViewById(R.id.etv_apikey) + val sdkKeyEditText = dialogLayout.findViewById(R.id.etv_sdkkey) builder.setView(dialogLayout) builder.setPositiveButton("Initialize") { _, i -> @@ -182,7 +186,8 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { } dialog.show() } - private fun getProductIdList(productIdList: ArrayList){ + + private fun getProductIdList(productIdList: ArrayList) { CBPurchase.retrieveProducts( this, productIdList, @@ -190,12 +195,13 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { override fun onSuccess(productIDs: ArrayList) { CoroutineScope(Dispatchers.Main).launch { if (productIDs.size > 0) { - launchProductDetailsScreen(gson.toJson(productIDs)) + launchProductDetailsScreen(gson.toJson(productIDs)) } else { alertSuccess("Items not available to buy") } } } + override fun onError(error: CBException) { Log.e(javaClass.simpleName, "Error: ${error.message}") showDialog(getCBError(error)) @@ -203,6 +209,22 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { }) } + private fun restorePurchases() { + CBPurchase.restorePurchases( + context = this, inActivePurchases = false, + completionCallback = object : RestorePurchaseCallback { + override fun onSuccess(result: List) { + Log.i(javaClass.simpleName, "result : $result") + result.forEach { + Log.i(javaClass.simpleName, "status : ${it.store_status}") + } + } + + override fun onError(error: CBException) { + Log.i(javaClass.simpleName, "error : $error") + } + }) + } private fun alertListProductId(list: Array) { val builder = AlertDialog.Builder(this) @@ -215,7 +237,7 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { javaClass.simpleName, " Item clicked :" + list[which] + " position :" + which ) - val productIdList = ArrayList() + val productIdList = ArrayList() productIdList.add(list[which].trim()) getProductIdList(productIdList) } diff --git a/app/src/main/java/com/chargebee/example/util/CBMenu.kt b/app/src/main/java/com/chargebee/example/util/CBMenu.kt index 93d52ac..41b3bab 100644 --- a/app/src/main/java/com/chargebee/example/util/CBMenu.kt +++ b/app/src/main/java/com/chargebee/example/util/CBMenu.kt @@ -12,6 +12,7 @@ enum class CBMenu(val value: String) { GetProducts("Get Products"), SubsStatus("Get Subscription Status"), SubsList("Get Subscriptions List"), - GetEntitlements("Get Entitlements") + GetEntitlements("Get Entitlements"), + RestorePurchase("Restore Purchase") } 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 2ddb7e9..4334fa0 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -4,42 +4,47 @@ import android.app.Activity import android.content.Context import android.os.Handler import android.os.Looper -import android.text.TextUtils import android.util.Log import com.android.billingclient.api.* import com.android.billingclient.api.BillingClient.BillingResponseCode.* import com.chargebee.android.ErrorDetail +import com.chargebee.android.billingservice.RestoreErrorCode.Companion.throwCBException +import com.chargebee.android.models.PurchaseTransaction import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.CBProduct import com.chargebee.android.network.CBReceiptResponse -import java.util.* +import com.chargebee.android.restore.CBRestorePurchaseManager +import kotlin.collections.ArrayList -class BillingClientManager constructor( - context: Context, skuType: String, - skuList: ArrayList, callBack: CBCallback.ListProductsCallback> -) : BillingClientStateListener, PurchasesUpdatedListener { +class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListener { private val CONNECT_TIMER_START_MILLISECONDS = 1L * 1000L - lateinit var billingClient: BillingClient - var mContext : Context? = null + internal var billingClient: BillingClient? = null + var mContext: Context? = null private val handler = Handler(Looper.getMainLooper()) - private var skuType : String? = null + private var skuType: String? = null private var skuList = arrayListOf() - private var callBack : CBCallback.ListProductsCallback> + private lateinit var callBack: CBCallback.ListProductsCallback> private var purchaseCallBack: CBCallback.PurchaseCallback? = null private val skusWithSkuDetails = arrayListOf() private val TAG = javaClass.simpleName lateinit var product: CBProduct - init { + constructor( + context: Context, skuType: String, + skuList: ArrayList, callBack: CBCallback.ListProductsCallback> + ) { mContext = context this.skuList = skuList - this.skuType =skuType + this.skuType = skuType this.callBack = callBack startBillingServiceConnection() } + constructor(context: Context) { + this.mContext = context + } /* Called to notify that the connection to the billing service was lost*/ override fun onBillingServiceDisconnected() { @@ -49,32 +54,39 @@ class BillingClientManager constructor( /* The listener method will be called when the billing client setup process complete */ override fun onBillingSetupFinished(billingResult: BillingResult) { when (billingResult.responseCode) { - BillingClient.BillingResponseCode.OK -> { + OK -> { Log.i( TAG, "Google Billing Setup Done!" ) loadProductDetails(BillingClient.SkuType.SUBS, skuList, callBack) } - BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED, - BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> { - callBack.onError(CBException(ErrorDetail(message = GPErrorCode.BillingUnavailable.errorMsg, httpStatusCode = billingResult.responseCode))) + FEATURE_NOT_SUPPORTED, + BILLING_UNAVAILABLE -> { + callBack.onError( + CBException( + ErrorDetail( + message = GPErrorCode.BillingUnavailable.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) Log.i(TAG, "onBillingSetupFinished() -> with error: ${billingResult.debugMessage}") } - BillingClient.BillingResponseCode.SERVICE_DISCONNECTED, - BillingClient.BillingResponseCode.USER_CANCELED, - BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE, - BillingClient.BillingResponseCode.ITEM_UNAVAILABLE, - BillingClient.BillingResponseCode.ERROR, - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED, - BillingClient.BillingResponseCode.SERVICE_TIMEOUT, - BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> { + SERVICE_DISCONNECTED, + USER_CANCELED, + SERVICE_UNAVAILABLE, + ITEM_UNAVAILABLE, + ERROR, + ITEM_ALREADY_OWNED, + SERVICE_TIMEOUT, + ITEM_NOT_OWNED -> { Log.i( TAG, "onBillingSetupFinished() -> google billing client error: ${billingResult.debugMessage}" ) } - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> { + DEVELOPER_ERROR -> { Log.i( TAG, "onBillingSetupFinished() -> Client is already in the process of connecting to billing service" @@ -89,19 +101,15 @@ class BillingClientManager constructor( /* Method used to configure and create a instance of billing client */ private fun startBillingServiceConnection() { - billingClient = mContext?.let { - BillingClient.newBuilder(it) - .enablePendingPurchases() - .setListener(this).build() - }!! - + buildBillingClient(this) connectToBillingService() } + /* Connect the billing client service */ private fun connectToBillingService() { - if (!billingClient.isReady) { + if (billingClient?.isReady == false) { handler.postDelayed( - { billingClient.startConnection(this@BillingClientManager) }, + { billingClient?.startConnection(this@BillingClientManager) }, CONNECT_TIMER_START_MILLISECONDS ) } @@ -112,44 +120,58 @@ class BillingClientManager constructor( @BillingClient.SkuType skuType: String, skuList: ArrayList, callBack: CBCallback.ListProductsCallback> ) { - try { - val params = SkuDetailsParams - .newBuilder() - .setSkusList(skuList) - .setType(skuType) - .build() + try { + val params = SkuDetailsParams + .newBuilder() + .setSkusList(skuList) + .setType(skuType) + .build() - billingClient.querySkuDetailsAsync( - params - ) { billingResult, skuDetailsList -> - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList != null) { - try { - skusWithSkuDetails.clear() - for (skuProduct in skuDetailsList) { - val product = CBProduct( - skuProduct.sku, - skuProduct.title, - skuProduct.price, - skuProduct, - false - ) - skusWithSkuDetails.add(product) - } - Log.i(TAG, "Product details :$skusWithSkuDetails") - callBack.onSuccess(productIDs = skusWithSkuDetails) - }catch (ex: CBException){ - callBack.onError(CBException(ErrorDetail(message = "Error while parsing data", httpStatusCode = billingResult.responseCode))) - Log.e(TAG, "exception :" + ex.message) - } - }else{ - Log.e(TAG, "Response Code :" + billingResult.responseCode) - callBack.onError(CBException(ErrorDetail(message = GPErrorCode.PlayServiceUnavailable.errorMsg, httpStatusCode = billingResult.responseCode))) - } - } - }catch (exp: CBException){ - Log.e(TAG, "exception :$exp.message") - callBack.onError(CBException(ErrorDetail(message = "${exp.message}"))) - } + billingClient?.querySkuDetailsAsync( + params + ) { billingResult, skuDetailsList -> + if (billingResult.responseCode == OK && skuDetailsList != null) { + try { + skusWithSkuDetails.clear() + for (skuProduct in skuDetailsList) { + val product = CBProduct( + skuProduct.sku, + skuProduct.title, + skuProduct.price, + skuProduct, + false + ) + skusWithSkuDetails.add(product) + } + Log.i(TAG, "Product details :$skusWithSkuDetails") + callBack.onSuccess(productIDs = skusWithSkuDetails) + } catch (ex: CBException) { + callBack.onError( + CBException( + ErrorDetail( + message = "Error while parsing data", + httpStatusCode = billingResult.responseCode + ) + ) + ) + Log.e(TAG, "exception :" + ex.message) + } + } else { + Log.e(TAG, "Response Code :" + billingResult.responseCode) + callBack.onError( + CBException( + ErrorDetail( + message = GPErrorCode.PlayServiceUnavailable.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) + } + } + } catch (exp: CBException) { + Log.e(TAG, "exception :$exp.message") + callBack.onError(CBException(ErrorDetail(message = "${exp.message}"))) + } } @@ -166,24 +188,36 @@ class BillingClientManager constructor( .setSkuDetails(skuDetails) .build() - billingClient.launchBillingFlow(mContext as Activity, params) - .takeIf { billingResult -> billingResult.responseCode != BillingClient.BillingResponseCode.OK + billingClient?.launchBillingFlow(mContext as Activity, params) + .takeIf { billingResult -> + 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))) + purchaseCallBack.onError( + CBException( + ErrorDetail( + message = GPErrorCode.LaunchBillingFlowError.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } + } + fun restorePurchases(completionCallback: RestorePurchaseCallback) { + queryPurchaseHistoryFromStore(completionCallback) } /* Checks if the specified feature is supported by the Play Store */ fun isFeatureSupported(): Boolean { try { - val featureSupportedResult = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS) - when(featureSupportedResult.responseCode){ - BillingClient.BillingResponseCode.OK -> { + val featureSupportedResult = + billingClient?.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS) + when (featureSupportedResult?.responseCode) { + OK -> { return true } - BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> { + FEATURE_NOT_SUPPORTED -> { return false } } @@ -194,12 +228,15 @@ class BillingClientManager constructor( } /* Checks if the billing client connected to the service */ - fun isBillingClientReady(): Boolean{ - return billingClient.isReady + fun isBillingClientReady(): Boolean? { + return billingClient?.isReady } /* Google Play calls this method to deliver the result of the Purchase Process/Operation */ - override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList?) { + override fun onPurchasesUpdated( + billingResult: BillingResult, + purchases: MutableList? + ) { when (billingResult.responseCode) { OK -> { purchases?.forEach { purchase -> @@ -208,56 +245,140 @@ class BillingClientManager constructor( acknowledgePurchase(purchase) } Purchase.PurchaseState.PENDING -> { - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchasePending.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.PurchasePending.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } Purchase.PurchaseState.UNSPECIFIED_STATE -> { - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseUnspecified.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.PurchaseUnspecified.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } } } } ITEM_ALREADY_OWNED -> { Log.e(TAG, "Billing response code : ITEM_ALREADY_OWNED") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.ProductAlreadyOwned.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.ProductAlreadyOwned.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } SERVICE_DISCONNECTED -> { connectToBillingService() } ITEM_UNAVAILABLE -> { Log.e(TAG, "Billing response code : ITEM_UNAVAILABLE") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.ProductUnavailable.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.ProductUnavailable.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } - USER_CANCELED ->{ + USER_CANCELED -> { Log.e(TAG, "Billing response code : USER_CANCELED ") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.CanceledPurchase.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.CanceledPurchase.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } - ITEM_NOT_OWNED ->{ + ITEM_NOT_OWNED -> { Log.e(TAG, "Billing response code : ITEM_NOT_OWNED ") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.ProductNotOwned.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.ProductNotOwned.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } SERVICE_TIMEOUT -> { Log.e(TAG, "Billing response code :SERVICE_TIMEOUT ") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PlayServiceTimeOut.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.PlayServiceTimeOut.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } SERVICE_UNAVAILABLE -> { Log.e(TAG, "Billing response code: SERVICE_UNAVAILABLE") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PlayServiceUnavailable.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.PlayServiceUnavailable.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } ERROR -> { Log.e(TAG, "Billing response code: ERROR") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.UnknownError.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.UnknownError.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } DEVELOPER_ERROR -> { Log.e(TAG, "Billing response code: DEVELOPER_ERROR") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.DeveloperError.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.DeveloperError.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } BILLING_UNAVAILABLE -> { Log.e(TAG, "Billing response code: BILLING_UNAVAILABLE") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.BillingUnavailable.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.BillingUnavailable.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } FEATURE_NOT_SUPPORTED -> { Log.e(TAG, "Billing response code: FEATURE_NOT_SUPPORTED") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.FeatureNotSupported.errorMsg, httpStatusCode = billingResult.responseCode))) + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.FeatureNotSupported.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) } } } @@ -268,13 +389,20 @@ class BillingClientManager constructor( val params = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() - billingClient.acknowledgePurchase(params) { billingResult -> - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { + billingClient?.acknowledgePurchase(params) { billingResult -> + if (billingResult.responseCode == OK) { try { - if (purchase.purchaseToken.isEmpty()){ + if (purchase.purchaseToken.isEmpty()) { Log.e(TAG, "Receipt Not Found") - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseReceiptNotFound.errorMsg, httpStatusCode = billingResult.responseCode))) - }else { + purchaseCallBack?.onError( + CBException( + ErrorDetail( + message = GPErrorCode.PurchaseReceiptNotFound.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + ) + } else { Log.i(TAG, "Google Purchase - success") Log.i(TAG, "Purchase Token -${purchase.purchaseToken}") validateReceipt(purchase.purchaseToken, product) @@ -287,69 +415,135 @@ class BillingClientManager constructor( } } } - } /* Chargebee method called here to validate receipt */ private fun validateReceipt(purchaseToken: String, product: CBProduct) { - try{ - CBPurchase.validateReceipt(purchaseToken, product) { - when(it) { - is ChargebeeResult.Success -> { - Log.i( - TAG, - "Validate Receipt Response: ${(it.data as CBReceiptResponse).in_app_subscription}" - ) - if (it.data.in_app_subscription != null){ - val subscriptionId = (it.data).in_app_subscription.subscription_id - Log.i(TAG, "Subscription ID: $subscriptionId") - val subscriptionResult = (it.data).in_app_subscription - if (subscriptionId.isEmpty()) { - purchaseCallBack?.onSuccess(subscriptionResult, false) + try { + CBPurchase.validateReceipt(purchaseToken, product) { + when (it) { + is ChargebeeResult.Success -> { + Log.i( + TAG, + "Validate Receipt Response: ${(it.data as CBReceiptResponse).in_app_subscription}" + ) + if (it.data.in_app_subscription != null) { + val subscriptionId = (it.data).in_app_subscription.subscription_id + Log.i(TAG, "Subscription ID: $subscriptionId") + val subscriptionResult = (it.data).in_app_subscription + if (subscriptionId.isEmpty()) { + purchaseCallBack?.onSuccess(subscriptionResult, false) + } else { + purchaseCallBack?.onSuccess(subscriptionResult, true) + } } else { - purchaseCallBack?.onSuccess(subscriptionResult, true) + purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) } - }else{ - purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) } - } - is ChargebeeResult.Error -> { - Log.e(TAG, "Exception from Server - validateReceipt() : ${it.exp.message}") - purchaseCallBack?.onError(it.exp) + is ChargebeeResult.Error -> { + Log.e(TAG, "Exception from Server - validateReceipt() : ${it.exp.message}") + purchaseCallBack?.onError(it.exp) + } } } - } - }catch (exp: Exception){ + } catch (exp: Exception) { Log.e(TAG, "Exception from Server- validateReceipt() : ${exp.message}") purchaseCallBack?.onError(CBException(ErrorDetail(message = exp.message))) } } - fun queryAllPurchases(){ - billingClient.queryPurchasesAsync( - BillingClient.SkuType.SUBS - ) { billingResult, activeSubsList -> - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { - Log.i(TAG, "queryAllPurchases :$activeSubsList") + private fun queryPurchaseHistoryFromStore(completionCallback: RestorePurchaseCallback) { + onConnected({ status -> + if (status) queryPurchaseHistory({ purchaseHistoryList -> + val storeTransactions = arrayListOf() + storeTransactions.addAll(purchaseHistoryList) + CBRestorePurchaseManager.fetchStoreSubscriptionStatus(storeTransactions, completionCallback) + }, { error -> completionCallback.onError(error) }) + }, { error -> + completionCallback.onError(error) + }) + } + + private fun queryPurchaseHistory( + completionCallback: (List) -> Unit, + connectionError: (CBException) -> Unit + ) { + queryAllPurchaseHistory(CBPurchase.ProductType.SUBS.value, { subscriptionTransactionList -> + queryAllPurchaseHistory( + CBPurchase.ProductType.INAPP.value, + { inAppPurchaseHistoryList -> + val purchaseTransactionHistory = inAppPurchaseHistoryList?.let { + subscriptionTransactionList?.plus(it) + } + completionCallback(purchaseTransactionHistory ?: emptyList()) + }, + { purchaseError -> connectionError(purchaseError) }) + }, { purchaseError -> connectionError(purchaseError) }) + } + + private fun queryAllPurchaseHistory( + productType: String, purchaseTransactionList: (List?) -> Unit, + purchaseError: (CBException) -> Unit + ) { + billingClient?.queryPurchaseHistoryAsync(productType) { billingResult, subsHistoryList -> + if (billingResult.responseCode == OK) { + val purchaseHistoryList = subsHistoryList?.map { + it.toPurchaseTransaction(productType) + } + purchaseTransactionList(purchaseHistoryList) } else { - Log.i( - TAG, - "queryAllPurchases :${billingResult.debugMessage}" - ) + purchaseError(throwCBException(billingResult)) } } } - fun queryPurchaseHistory(){ - billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.SUBS){ billingResult, subsHistoryList -> - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { - Log.i(TAG, "queryPurchaseHistory :$subsHistoryList") - } else { - Log.i( - TAG, - "queryPurchaseHistory :${billingResult.debugMessage}" - ) + private fun PurchaseHistoryRecord.toPurchaseTransaction(productType: String): PurchaseTransaction { + return PurchaseTransaction( + productId = this.skus, + productType = productType, + purchaseTime = this.purchaseTime, + purchaseToken = this.purchaseToken + ) + } + + private fun buildBillingClient(listener: PurchasesUpdatedListener): BillingClient? { + if (billingClient == null) { + billingClient = mContext?.let { + BillingClient.newBuilder(it).enablePendingPurchases().setListener(listener) + .build() } } + return billingClient + } + + private fun onConnected(status: (Boolean) -> Unit, connectionError: (CBException) -> Unit) { + val billingClient = buildBillingClient(this) + if (billingClient?.isReady == false) { + handler.postDelayed({ + billingClient.startConnection(object : + BillingClientStateListener { + override fun onBillingServiceDisconnected() { + Log.i(javaClass.simpleName, "onBillingServiceDisconnected") + status(false) + } + + override fun onBillingSetupFinished(billingResult: BillingResult) { + when (billingResult.responseCode) { + OK -> { + Log.i( + TAG, + "Google Billing Setup Done!" + ) + status(true) + } + else -> { + connectionError(throwCBException(billingResult)) + } + } + + } + }) + }, CONNECT_TIMER_START_MILLISECONDS) + } else status(true) } } 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 9126822..975a8e9 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt @@ -1,7 +1,7 @@ package com.chargebee.android.billingservice import com.chargebee.android.exceptions.CBException -import com.chargebee.android.models.CBProduct +import com.chargebee.android.models.* import com.chargebee.android.network.ReceiptDetail interface CBCallback { @@ -17,5 +17,9 @@ interface CBCallback { fun onSuccess(result: ReceiptDetail, status: Boolean) fun onError(error: CBException) } +} +interface RestorePurchaseCallback { + fun onSuccess(result: List) + 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 3ebb4a6..d83c9bd 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -13,12 +13,18 @@ import com.chargebee.android.models.ResultHandler import com.chargebee.android.network.* import com.chargebee.android.resources.CatalogVersion import com.chargebee.android.resources.ReceiptResource -import java.util.ArrayList + object CBPurchase { var billingClientManager: BillingClientManager? = null val productIdList = arrayListOf() private var customer : CBCustomer? = null + var inActivePurchases = false + + enum class ProductType(val value: String) { + SUBS("subs"), + INAPP("inapp") + } annotation class SkuType { companion object { @@ -96,23 +102,16 @@ object CBPurchase { } } + @JvmStatic + fun restorePurchases(context: Context, inActivePurchases: Boolean = false, completionCallback: RestorePurchaseCallback){ + this.inActivePurchases = inActivePurchases + shareInstance(context).restorePurchases(completionCallback) + } /* Chargebee Method - used to validate the receipt of purchase */ @JvmStatic fun validateReceipt(purchaseToken: String, product: CBProduct, completion : (ChargebeeResult) -> Unit) { try { - val logger = CBLogger(name = "buy", action = "process_purchase_command") - val params = Params( - purchaseToken, - product.productId, - customer, - Chargebee.channel - ) - - ResultHandler.safeExecuter( - { ReceiptResource().validateReceipt(params) }, - completion, - logger - ) + validateReceipt(purchaseToken, product.productId, completion) }catch (exp: Exception){ Log.e(javaClass.simpleName, "Exception in validateReceipt() :"+exp.message) ChargebeeResult.Error( @@ -124,37 +123,21 @@ object CBPurchase { ) } } - @JvmStatic - @Throws(InvalidRequestException::class, OperationFailedException::class) - fun queryPurchaseHistory() { - try { - billingClientManager?.queryPurchaseHistory() - }catch (exp: Exception){ - Log.i(javaClass.simpleName, "Exception in validateReceipt() :"+exp.message) - ChargebeeResult.Error( - exp = CBException( - error = ErrorDetail( - exp.message - ) - ) - ) - } - } - @JvmStatic - @Throws(InvalidRequestException::class, OperationFailedException::class) - fun queryAllPurchases() { - try { - billingClientManager?.queryAllPurchases() - }catch (exp: Exception){ - Log.i(javaClass.simpleName, "Exception in validateReceipt() :"+exp.message) - ChargebeeResult.Error( - exp = CBException( - error = ErrorDetail( - exp.message - ) - ) - ) - } + + fun validateReceipt(purchaseToken: String, productId: String, completion : (ChargebeeResult) -> Unit){ + val logger = CBLogger(name = "buy", action = "process_purchase_command") + val params = Params( + purchaseToken, + productId, + customer, + Chargebee.channel + ) + + ResultHandler.safeExecuter( + { ReceiptResource().validateReceipt(params) }, + completion, + logger + ) } /* @@ -240,5 +223,10 @@ object CBPurchase { if (arr.size==1) list.add("Standard") return list.toTypedArray() } - + private fun shareInstance(context: Context): BillingClientManager { + if (billingClientManager == null) { + billingClientManager = BillingClientManager(context) + } + return billingClientManager as BillingClientManager + } } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt index ba38426..4172a00 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt @@ -1,5 +1,10 @@ package com.chargebee.android.billingservice +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingResult +import com.chargebee.android.ErrorDetail +import com.chargebee.android.exceptions.CBException + enum class GPErrorCode(val errorMsg: String) { BillingUnavailable("The Billing API version is not supported"), PurchasePending("Purchase is in pending state"), @@ -18,4 +23,59 @@ enum class GPErrorCode(val errorMsg: String) { DeveloperError("Invalid arguments provided to the API"), BillingClientNotReady("Play services not available"), SDKKeyNotAvailable("SDK key not available to proceed purchase"), +} + +internal enum class RestoreErrorCode(val code: Int) { + UNKNOWN(-4), + SERVICE_TIMEOUT(-3), + FEATURE_NOT_SUPPORTED(-2), + USER_CANCELED(1), + SERVICE_UNAVAILABLE(2), + BILLING_UNAVAILABLE(3), + ITEM_UNAVAILABLE(4), + DEVELOPER_ERROR(5), + ERROR(6), + ITEM_NOT_OWNED(8); + + companion object { + private fun billingResponseCode(responseCode: Int): RestoreErrorCode = + when (responseCode) { + BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> SERVICE_TIMEOUT + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> FEATURE_NOT_SUPPORTED + BillingClient.BillingResponseCode.USER_CANCELED -> USER_CANCELED + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> SERVICE_UNAVAILABLE + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> BILLING_UNAVAILABLE + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> ITEM_UNAVAILABLE + BillingClient.BillingResponseCode.DEVELOPER_ERROR -> DEVELOPER_ERROR + BillingClient.BillingResponseCode.ERROR -> ERROR + BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> ITEM_NOT_OWNED + else -> { + UNKNOWN + } + } + + private fun billingDebugMessage(responseCode: Int): GPErrorCode = + when (responseCode) { + BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> GPErrorCode.PlayServiceTimeOut + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> GPErrorCode.FeatureNotSupported + BillingClient.BillingResponseCode.USER_CANCELED -> GPErrorCode.CanceledPurchase + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> GPErrorCode.PlayServiceUnavailable + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> GPErrorCode.BillingUnavailable + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> GPErrorCode.ProductUnavailable + BillingClient.BillingResponseCode.DEVELOPER_ERROR -> GPErrorCode.DeveloperError + BillingClient.BillingResponseCode.ERROR -> GPErrorCode.UnknownError + BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> GPErrorCode.ProductNotOwned + else -> { + GPErrorCode.UnknownError + } + } + + fun throwCBException(billingResult: BillingResult): CBException = + CBException( + ErrorDetail( + httpStatusCode = billingResponseCode(billingResult.responseCode).code, + message = billingDebugMessage(billingResult.responseCode).errorMsg + ) + ) + } } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt new file mode 100644 index 0000000..94de66e --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt @@ -0,0 +1,10 @@ +package com.chargebee.android.models + +data class CBRestoreSubscription(val subscription_id: String, val plan_id: String, val store_status: StoreStatus) +data class CBRestorePurchases(val in_app_subscriptions: ArrayList) + +enum class StoreStatus{ + active, + in_trial, + cancelled +} \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/models/PurchaseTransaction.kt b/chargebee/src/main/java/com/chargebee/android/models/PurchaseTransaction.kt new file mode 100644 index 0000000..291eaa7 --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/models/PurchaseTransaction.kt @@ -0,0 +1,8 @@ +package com.chargebee.android.models + +data class PurchaseTransaction( + val productId: List, + val purchaseTime: Long, + val purchaseToken: String, + val productType: String +) diff --git a/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt b/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt index 1e20f71..ee84ea4 100644 --- a/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt +++ b/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt @@ -1,10 +1,8 @@ package com.chargebee.android.repository import com.chargebee.android.Chargebee -import com.chargebee.android.models.CBEntitlements -import com.chargebee.android.models.CBSubscription +import com.chargebee.android.models.* import com.chargebee.android.models.KeyValidationWrapper -import com.chargebee.android.models.SubscriptionDetailsWrapper import retrofit2.Response import retrofit2.http.* @@ -40,4 +38,13 @@ internal interface PurchaseRepository { @Header("version") sdkVersion: String = Chargebee.sdkVersion, @Path("subscription_id") subscriptionId: String ): Response + + @FormUrlEncoded + @POST("v2/in_app_subscriptions/{sdkKey}/retrieve") + suspend fun retrieveRestoreSubscription( + @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/RestorePurchaseResource.kt b/chargebee/src/main/java/com/chargebee/android/resources/RestorePurchaseResource.kt new file mode 100644 index 0000000..b3ce0ab --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/resources/RestorePurchaseResource.kt @@ -0,0 +1,24 @@ +package com.chargebee.android.resources + +import com.chargebee.android.Chargebee +import com.chargebee.android.exceptions.ChargebeeResult +import com.chargebee.android.repository.PurchaseRepository +import com.chargebee.android.responseFromServer + +internal class RestorePurchaseResource : BaseResource(Chargebee.baseUrl) { + + internal suspend fun retrieveStoreSubscription(purchaseToken: String): ChargebeeResult { + val dataMap = convertToMap(purchaseToken) + val response = apiClient.create(PurchaseRepository::class.java) + .retrieveRestoreSubscription(data = dataMap) + return responseFromServer( + response + ) + } + + private fun convertToMap(receipt: String): Map { + return mapOf( + "receipt" to receipt + ) + } +} \ 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 new file mode 100644 index 0000000..54dfab2 --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -0,0 +1,134 @@ +package com.chargebee.android.restore + +import android.util.Log +import com.chargebee.android.Chargebee +import com.chargebee.android.ErrorDetail +import com.chargebee.android.billingservice.CBPurchase +import com.chargebee.android.billingservice.RestorePurchaseCallback +import com.chargebee.android.exceptions.CBException +import com.chargebee.android.exceptions.ChargebeeResult +import com.chargebee.android.loggers.CBLogger +import com.chargebee.android.models.* +import com.chargebee.android.models.ResultHandler +import com.chargebee.android.resources.RestorePurchaseResource + + +class CBRestorePurchaseManager { + + companion object { + private var allTransactions = ArrayList() + private var restorePurchases = ArrayList() + private var activeTransactions = ArrayList() + + private fun retrieveStoreSubscription( + purchaseToken: String, + completion: (ChargebeeResult) -> Unit + ) { + try { + val logger = CBLogger(name = "restore", action = "retrieve_restore_subscriptions") + ResultHandler.safeExecuter( + { RestorePurchaseResource().retrieveStoreSubscription(purchaseToken) }, + completion, + logger + ) + } catch (exp: Exception) { + ChargebeeResult.Error( + exp = CBException( + error = ErrorDetail( + exp.message + ) + ) + ) + } + } + + private fun retrieveRestoreSubscription( + purchaseToken: String, + result: (CBRestoreSubscription) -> Unit, + error: (CBException) -> Unit + ) { + retrieveStoreSubscription(purchaseToken) { + when (it) { + is ChargebeeResult.Success -> { + val restoreSubscription = + ((it.data) as CBRestorePurchases).in_app_subscriptions.firstOrNull() + restoreSubscription?.let { + result(restoreSubscription) + } + } + is ChargebeeResult.Error -> { + error(it.exp) + } + } + } + } + + fun fetchStoreSubscriptionStatus( + storeTransactions: ArrayList, + completionCallback: RestorePurchaseCallback + ) { + val storeTransaction = + storeTransactions.firstOrNull()?.also { storeTransactions.remove(it) } + storeTransaction?.purchaseToken?.let { purchaseToken -> + retrieveRestoreSubscription(purchaseToken, { + restorePurchases.add(it) + when(it.store_status){ + StoreStatus.active -> activeTransactions.add(storeTransaction) + else -> allTransactions.add(storeTransaction) + } + getRestorePurchases(storeTransactions, completionCallback) + }, { error -> + getRestorePurchases(storeTransactions, completionCallback) + completionCallback.onError(error) + }) + } + } + + private fun getRestorePurchases( storeTransactions: ArrayList, completionCallback: RestorePurchaseCallback){ + if (storeTransactions.isEmpty()) { + val activePurchases = restorePurchases.filter { subscription -> + subscription.store_status == StoreStatus.active + } + val allPurchases = restorePurchases.filter { subscription -> + subscription.store_status == StoreStatus.active || subscription.store_status == StoreStatus.in_trial + || subscription.store_status == StoreStatus.cancelled + } + if (CBPurchase.inActivePurchases) { + completionCallback.onSuccess(activePurchases) + syncPurchaseWithChargebee(activeTransactions) + }else { + completionCallback.onSuccess(allPurchases) + syncPurchaseWithChargebee(allTransactions) + } + }else { + fetchStoreSubscriptionStatus(storeTransactions, completionCallback) + } + } + + private fun syncPurchaseWithChargebee(storeTransactions: ArrayList) { + storeTransactions.forEach { productIdList -> + if (productIdList.productType == CBPurchase.ProductType.SUBS.value) { + validateReceipt(productIdList.purchaseToken, productIdList.productId.first()) + } else { + TODO ("Handle one time purchases here") + } + } + } + + private fun validateReceipt(purchaseToken: String, productId: String) { + CBPurchase.validateReceipt(purchaseToken, productId) { + when (it) { + is ChargebeeResult.Success -> { + Log.i(javaClass.simpleName, "result : ${it.data}") + } + is ChargebeeResult.Error -> { + Log.e( + javaClass.simpleName, + "Exception from Server - validateReceipt() : ${it.exp.message}" + ) + } + } + } + } + } +} \ No newline at end of file From 16d175c1446710a7e81d90e1758b0f6eac952166 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 2 May 2023 21:43:27 +0530 Subject: [PATCH 02/84] 1. Updated the test classes SubscriptionResourceTest.kt and ItemResourceTest.kt 2. Added unit test for restore purchases --- .../restore/CBRestorePurchaseManager.kt | 23 +- .../android/resources/ItemResourceTest.kt | 24 +- .../resources/SubscriptionResourceTest.kt | 6 +- .../android/restore/RestorePurchaseTest.kt | 260 ++++++++++++++++++ 4 files changed, 288 insertions(+), 25 deletions(-) create mode 100644 chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt 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 54dfab2..e1faee2 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -19,6 +19,7 @@ class CBRestorePurchaseManager { private var allTransactions = ArrayList() private var restorePurchases = ArrayList() private var activeTransactions = ArrayList() + lateinit var completionCallback: RestorePurchaseCallback private fun retrieveStoreSubscription( purchaseToken: String, @@ -42,7 +43,7 @@ class CBRestorePurchaseManager { } } - private fun retrieveRestoreSubscription( + internal fun retrieveRestoreSubscription( purchaseToken: String, result: (CBRestoreSubscription) -> Unit, error: (CBException) -> Unit @@ -63,10 +64,11 @@ class CBRestorePurchaseManager { } } - fun fetchStoreSubscriptionStatus( + internal fun fetchStoreSubscriptionStatus( storeTransactions: ArrayList, completionCallback: RestorePurchaseCallback ) { + this.completionCallback = completionCallback val storeTransaction = storeTransactions.firstOrNull()?.also { storeTransactions.remove(it) } storeTransaction?.purchaseToken?.let { purchaseToken -> @@ -76,15 +78,16 @@ class CBRestorePurchaseManager { StoreStatus.active -> activeTransactions.add(storeTransaction) else -> allTransactions.add(storeTransaction) } - getRestorePurchases(storeTransactions, completionCallback) + getRestorePurchases(storeTransactions) }, { error -> - getRestorePurchases(storeTransactions, completionCallback) + getRestorePurchases(storeTransactions) + completionCallback.onError(error) }) } } - private fun getRestorePurchases( storeTransactions: ArrayList, completionCallback: RestorePurchaseCallback){ + internal fun getRestorePurchases( storeTransactions: ArrayList){ if (storeTransactions.isEmpty()) { val activePurchases = restorePurchases.filter { subscription -> subscription.store_status == StoreStatus.active @@ -105,17 +108,13 @@ class CBRestorePurchaseManager { } } - private fun syncPurchaseWithChargebee(storeTransactions: ArrayList) { + internal fun syncPurchaseWithChargebee(storeTransactions: ArrayList) { storeTransactions.forEach { productIdList -> - if (productIdList.productType == CBPurchase.ProductType.SUBS.value) { - validateReceipt(productIdList.purchaseToken, productIdList.productId.first()) - } else { - TODO ("Handle one time purchases here") - } + validateReceipt(productIdList.purchaseToken, productIdList.productId.first()) } } - private fun validateReceipt(purchaseToken: String, productId: String) { + internal fun validateReceipt(purchaseToken: String, productId: String) { CBPurchase.validateReceipt(purchaseToken, productId) { when (it) { is ChargebeeResult.Success -> { diff --git a/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt b/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt index 2f85655..fff77f4 100644 --- a/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt @@ -42,10 +42,14 @@ class ItemResourceTest { @Test fun test_retrieveItemsList_success(){ - val item = Items("123","item","active","play_store") + val item = Items( + "id", "name", "invoice", "play_store", "123", "", 0, + "12", "23", false, "false", false, false, "false", + false, "false" + ) val queryParam = arrayOf("Standard", "app_store") val lock = CountDownLatch(1) - Items.retrieveAllItems(queryParam) { + Chargebee.retrieveAllItems(queryParam) { when (it) { is ChargebeeResult.Success -> { lock.countDown() @@ -77,7 +81,7 @@ class ItemResourceTest { val exception = CBException(ErrorDetail("Error")) val queryParam = arrayOf("Standard", "app_store") val lock = CountDownLatch(1) - Items.retrieveAllItems(queryParam) { + Chargebee.retrieveAllItems(queryParam) { when (it) { is ChargebeeResult.Success -> { lock.countDown() @@ -106,15 +110,15 @@ class ItemResourceTest { } @Test fun test_retrieveItem_success(){ - val plan = Plan( - "id", "name", "invoice", 123, 123, "", "", - 12, 23, "", false, false, "false", false, - 9, false, "app_store", 7, "", "", false, "", false, false + val item = Items( + "id", "name", "invoice", "play_store", "123", "", 0, + "12", "23", false, "false", false, false, "false", + false, "false" ) val queryParam = "Standard" val lock = CountDownLatch(1) - Items.retrieveItem(queryParam) { + Chargebee.retrieveItem(queryParam) { when (it) { is ChargebeeResult.Success -> { lock.countDown() @@ -135,7 +139,7 @@ class ItemResourceTest { CoroutineScope(Dispatchers.IO).launch { Mockito.`when`(ItemsResource().retrieveItem(queryParam)).thenReturn( ChargebeeResult.Success( - plan + item ) ) Mockito.verify(ItemsResource(), Mockito.times(1)).retrieveItem(queryParam) @@ -146,7 +150,7 @@ class ItemResourceTest { val exception = CBException(ErrorDetail("Error")) val queryParam = "Standard" val lock = CountDownLatch(1) - Items.retrieveItem(queryParam) { + Chargebee.retrieveItem(queryParam) { when (it) { is ChargebeeResult.Success -> { lock.countDown() diff --git a/chargebee/src/test/java/com/chargebee/android/resources/SubscriptionResourceTest.kt b/chargebee/src/test/java/com/chargebee/android/resources/SubscriptionResourceTest.kt index 7a943a4..cc26bcb 100644 --- a/chargebee/src/test/java/com/chargebee/android/resources/SubscriptionResourceTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/resources/SubscriptionResourceTest.kt @@ -41,10 +41,10 @@ class SubscriptionResourceTest { fun test_subscriptionStatus_success(){ val subscriptionDetail = SubscriptionDetail("123","item","active","","", - "") + "","","") val queryParam = "0000987657" val lock = CountDownLatch(1) - SubscriptionDetail.retrieveSubscription(queryParam) { + Chargebee.retrieveSubscription(queryParam) { when (it) { is ChargebeeResult.Success -> { lock.countDown() @@ -76,7 +76,7 @@ class SubscriptionResourceTest { val exception = CBException(ErrorDetail("Error")) val queryParam = "0000987657" val lock = CountDownLatch(1) - SubscriptionDetail.retrieveSubscription(queryParam) { + Chargebee.retrieveSubscription(queryParam) { when (it) { is ChargebeeResult.Success -> { lock.countDown() diff --git a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt new file mode 100644 index 0000000..7364da8 --- /dev/null +++ b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt @@ -0,0 +1,260 @@ +package com.chargebee.android.restore + +import com.android.billingclient.api.* +import com.chargebee.android.Chargebee +import com.chargebee.android.ErrorDetail +import com.chargebee.android.billingservice.RestorePurchaseCallback +import com.chargebee.android.exceptions.CBException +import com.chargebee.android.exceptions.ChargebeeResult +import com.chargebee.android.models.* +import com.chargebee.android.network.* +import com.chargebee.android.resources.ReceiptResource +import com.chargebee.android.resources.RestorePurchaseResource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner +import java.util.concurrent.CountDownLatch + +@RunWith(MockitoJUnitRunner::class) +class RestorePurchaseTest { + private var allTransactions = ArrayList() + private var restorePurchases = ArrayList() + private var activeTransactions = ArrayList() + private var customer: CBCustomer? = null + private val list = ArrayList() + private val storeTransactions = arrayListOf() + private val lock = CountDownLatch(1) + private val response = + CBReceiptResponse(ReceiptDetail("subscriptionId", "customerId", "planId")) + private val error = CBException( + ErrorDetail( + message = "The Token data sent is not correct or Google service is temporarily down", + httpStatusCode = 400 + ) + ) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + customer = CBCustomer("test", "android", "test", "test@gmail.com") + list.add("chargebee.pro.android") + Chargebee.configure( + site = "omni1-test.integrations.predev37.in", + publishableApiKey = "test_rpKneFyplowONFtdHgnlpxh6ccdcQXNUcu", + sdkKey = "cb-hmg6jlyvyrahvocyio57oqhoei", + packageName = "com.chargebee.example" + ) + } + + @After + fun tearDown() { + allTransactions.clear() + restorePurchases.clear() + activeTransactions.clear() + customer = null + } + + @Test + fun test_fetchStoreSubscriptionStatus_success() { + val lock = CountDownLatch(1) + val purchaseTransaction = getTransaction(true) + + CBRestorePurchaseManager.fetchStoreSubscriptionStatus( + purchaseTransaction, + completionCallback = object : RestorePurchaseCallback { + override fun onSuccess(result: List) { + lock.countDown() + result.forEach { + MatcherAssert.assertThat( + (it), + Matchers.instanceOf(CBRestoreSubscription::class.java) + ) + } + + } + + override fun onError(error: CBException) { + lock.countDown() + MatcherAssert.assertThat( + error, + Matchers.instanceOf(CBException::class.java) + ) + } + }) + lock.await() + } + + @Test + fun test_fetchStoreSubscriptionStatus_failure() { + val purchaseTransaction = getTransaction(false) + + val storeTransaction = + purchaseTransaction.firstOrNull()?.also { purchaseTransaction.remove(it) } + storeTransaction?.purchaseToken?.let { purchaseToken -> + CBRestorePurchaseManager.retrieveRestoreSubscription(purchaseToken, {}, { error -> + lock.countDown() + MatcherAssert.assertThat( + (error), + Matchers.instanceOf(CBException::class.java) + ) + Mockito.verify(CBRestorePurchaseManager, Mockito.times(1)) + .getRestorePurchases(purchaseTransaction) + }) + } + lock.await() + } + + @Test + fun test_retrieveStoreSubscription_success() { + val purchaseTransaction = getTransaction(true) + val cbRestorePurchasesList = arrayListOf() + val purchaseToken = purchaseTransaction.first().purchaseToken + val cbRestoreSubscription = CBRestoreSubscription("", "", StoreStatus.active) + cbRestorePurchasesList.add(cbRestoreSubscription) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(RestorePurchaseResource().retrieveStoreSubscription(purchaseToken)) + .thenReturn( + ChargebeeResult.Success( + CBRestorePurchases(cbRestorePurchasesList) + ) + ) + Mockito.verify(RestorePurchaseResource(), Mockito.times(1)) + .retrieveStoreSubscription(purchaseToken) + } + } + + @Test + fun test_retrieveStoreSubscription_failure() { + val purchaseTransaction = getTransaction(false) + val purchaseToken = purchaseTransaction.first().purchaseToken + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(RestorePurchaseResource().retrieveStoreSubscription(purchaseToken)) + .thenReturn( + ChargebeeResult.Error( + error + ) + ) + Mockito.verify(RestorePurchaseResource(), Mockito.times(1)) + .retrieveStoreSubscription(purchaseToken) + } + } + + @Test + fun test_validateReceipt_success() { + val purchaseTransaction = getTransaction(true) + val params = Params( + purchaseTransaction.first().purchaseToken, + purchaseTransaction.first().productId.first(), + customer, + Chargebee.channel + ) + CBRestorePurchaseManager.validateReceipt( + params.receipt, + purchaseTransaction.first().productType + ) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( + ChargebeeResult.Success( + response + ) + ) + Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + .toCBReceiptReqBody() + } + } + + @Test + fun test_validateReceipt_failure() { + val purchaseTransaction = getTransaction(false) + val params = Params( + purchaseTransaction.first().purchaseToken, + purchaseTransaction.first().productId.first(), + customer, + Chargebee.channel + ) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( + ChargebeeResult.Error( + error + ) + ) + Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + .toCBReceiptReqBody() + } + } + + @Test + fun test_syncPurchaseWithChargebee_success() { + val purchaseTransaction = getTransaction(false) + val params = Params( + purchaseTransaction.first().purchaseToken, + purchaseTransaction.first().productId.first(), + customer, + Chargebee.channel + ) + CBRestorePurchaseManager.syncPurchaseWithChargebee(purchaseTransaction) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( + ChargebeeResult.Success( + response + ) + ) + Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + .toCBReceiptReqBody() + } + } + + @Test + fun test_syncPurchaseWithChargebee_failure() { + val purchaseTransaction = getTransaction(false) + val params = Params( + purchaseTransaction.first().purchaseToken, + purchaseTransaction.first().productId.first(), + customer, + Chargebee.channel + ) + CBRestorePurchaseManager.syncPurchaseWithChargebee(purchaseTransaction) + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( + ChargebeeResult.Error( + error + ) + ) + Mockito.verify(ReceiptResource(), Mockito.times(1)).validateReceipt(params) + Mockito.verify(CBReceiptRequestBody("receipt", "", null, ""), Mockito.times(1)) + .toCBReceiptReqBody() + } + } + + private fun getTransaction(isTestingSuccess: Boolean): ArrayList { + storeTransactions.clear() + val result = if (isTestingSuccess) + PurchaseTransaction( + productId = list.toList(), + purchaseTime = 1682666112774, + purchaseToken = "fajeooclbamgohgapjeehghm.AO-J1OzxVvoEx7y53c9DsypEKwgcfGw2OrisyQsQ-MG6KiXfJ97nT33Yd5VpbQYxd225QnTAEVdPuLP4YSvZE6LBhsv1rzSlizuBxBTjBWghWguSBBtgp2g", + productType = "subs" + ) + else + PurchaseTransaction( + productId = list.toList(), + purchaseTime = 1682666112774, + purchaseToken = "test data", + productType = "subs" + ) + storeTransactions.add(result) + return storeTransactions + } +} \ No newline at end of file From 4680898c57b2c0c3c27fe7b15be86967a4feddcf Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 3 May 2023 17:15:58 +0530 Subject: [PATCH 03/84] Refactor: handled error on restore api and added paused enum in store status --- .../com/chargebee/example/MainActivity.kt | 17 ++++-- .../billingservice/BillingClientManager.kt | 8 ++- .../android/billingservice/CBPurchase.kt | 8 +++ .../android/billingservice/GPErrorCode.kt | 1 + .../android/models/CBRestoreSubscription.kt | 3 +- .../restore/CBRestorePurchaseManager.kt | 53 +++++++++++-------- 6 files changed, 63 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index 576c299..aa07584 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.launch class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { private var mItemsRecyclerView: RecyclerView? = null - private var list = arrayListOf() + private var list = arrayListOf() var listItemsAdapter: ListItemsAdapter? = null var featureList = mutableListOf() var mContext: Context? = null @@ -210,18 +210,27 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { } private fun restorePurchases() { + showProgressDialog() CBPurchase.restorePurchases( - context = this, inActivePurchases = false, + context = this, inActivePurchases = true, completionCallback = object : RestorePurchaseCallback { override fun onSuccess(result: List) { - Log.i(javaClass.simpleName, "result : $result") + hideProgressDialog() result.forEach { Log.i(javaClass.simpleName, "status : ${it.store_status}") } + CoroutineScope(Dispatchers.Main).launch { + if (result.isNotEmpty()) + alertSuccess("${result.size} purchases restored successfully") + } } override fun onError(error: CBException) { - Log.i(javaClass.simpleName, "error : $error") + hideProgressDialog() + Log.e(javaClass.simpleName, "error message: ${error.message}") + CoroutineScope(Dispatchers.Main).launch { + showDialog("${error.message}, ${error.httpStatusCode}") + } } }) } 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 4334fa0..9d4a234 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -204,7 +204,13 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene } } - fun restorePurchases(completionCallback: RestorePurchaseCallback) { + /** + * This method will provide all the purchases associated with the current account based on the [inActivePurchases] flag set. + * And the associated purchases can be synced with Chargebee. + * + * @param [completionCallback] The listener will be called when restore purchase completes. + */ + internal fun restorePurchases(completionCallback: RestorePurchaseCallback) { queryPurchaseHistoryFromStore(completionCallback) } 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 d83c9bd..3e4f918 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -102,6 +102,14 @@ object CBPurchase { } } + /** + * This method will provide all the purchases associated with the current account based on the [inActivePurchases] flag set. + * And the associated purchases can be synced with Chargebee. + * + * @param [context] Current activity context + * @param [inActivePurchases] False by default. if true, only active purchases restores and synced with Chargebee. + * @param [completionCallback] The listener will be called when restore purchase completes. + */ @JvmStatic fun restorePurchases(context: Context, inActivePurchases: Boolean = false, completionCallback: RestorePurchaseCallback){ this.inActivePurchases = inActivePurchases diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt index 4172a00..f68af79 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt @@ -23,6 +23,7 @@ enum class GPErrorCode(val errorMsg: String) { DeveloperError("Invalid arguments provided to the API"), BillingClientNotReady("Play services not available"), SDKKeyNotAvailable("SDK key not available to proceed purchase"), + InvalidPurchaseToken("The Token data sent is not correct or Google service is temporarily down") } internal enum class RestoreErrorCode(val code: Int) { diff --git a/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt index 94de66e..679a313 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt @@ -6,5 +6,6 @@ data class CBRestorePurchases(val in_app_subscriptions: ArrayList retrieveRestoreSubscription(purchaseToken, { restorePurchases.add(it) - when(it.store_status){ - StoreStatus.active -> activeTransactions.add(storeTransaction) - else -> allTransactions.add(storeTransaction) + when (it.store_status) { + StoreStatus.active -> activeTransactions.add(storeTransaction) + else -> allTransactions.add(storeTransaction) } getRestorePurchases(storeTransactions) - }, { error -> + }, { _ -> getRestorePurchases(storeTransactions) - - completionCallback.onError(error) }) } } - internal fun getRestorePurchases( storeTransactions: ArrayList){ + internal fun getRestorePurchases(storeTransactions: ArrayList) { if (storeTransactions.isEmpty()) { - val activePurchases = restorePurchases.filter { subscription -> - subscription.store_status == StoreStatus.active - } - val allPurchases = restorePurchases.filter { subscription -> - subscription.store_status == StoreStatus.active || subscription.store_status == StoreStatus.in_trial - || subscription.store_status == StoreStatus.cancelled - } - if (CBPurchase.inActivePurchases) { - completionCallback.onSuccess(activePurchases) - syncPurchaseWithChargebee(activeTransactions) - }else { - completionCallback.onSuccess(allPurchases) - syncPurchaseWithChargebee(allTransactions) + if(restorePurchases.isEmpty()) { + completionCallback.onError( + CBException( + ErrorDetail( + message = GPErrorCode.InvalidPurchaseToken.errorMsg, + httpStatusCode = 400 + ) + ) + ) + } else { + val activePurchases = restorePurchases.filter { subscription -> + subscription.store_status == StoreStatus.active + } + val allPurchases = restorePurchases.filter { subscription -> + subscription.store_status == StoreStatus.active || subscription.store_status == StoreStatus.in_trial + || subscription.store_status == StoreStatus.cancelled || subscription.store_status == StoreStatus.paused + } + if (CBPurchase.inActivePurchases) { + completionCallback.onSuccess(activePurchases) + syncPurchaseWithChargebee(activeTransactions) + } else { + completionCallback.onSuccess(allPurchases) + syncPurchaseWithChargebee(allTransactions) + } } - }else { + restorePurchases.clear() + } else { fetchStoreSubscriptionStatus(storeTransactions, completionCallback) } } From fbd02e78f3de3a92637bf04bb344ebbe406a1d8f Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 4 May 2023 09:32:02 +0530 Subject: [PATCH 04/84] Added empty checks on restore purchases --- app/src/main/java/com/chargebee/example/MainActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index aa07584..81f23e8 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -222,6 +222,9 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { CoroutineScope(Dispatchers.Main).launch { if (result.isNotEmpty()) alertSuccess("${result.size} purchases restored successfully") + else + alertSuccess("Purchases not found to restore") + } } From 0ba3b56a78d3ce1ab550af91afdebe906a194b97 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 4 May 2023 10:51:59 +0530 Subject: [PATCH 05/84] Addressed review comments --- .../com/chargebee/example/MainActivity.kt | 7 ++-- .../billingservice/BillingClientManager.kt | 4 +- .../android/billingservice/CBCallback.kt | 10 +++-- .../android/billingservice/CBPurchase.kt | 18 ++++---- .../android/models/CBRestoreSubscription.kt | 19 +++++++-- .../android/repository/PurchaseRepository.kt | 2 +- .../resources/RestorePurchaseResource.kt | 2 +- .../restore/CBRestorePurchaseManager.kt | 41 +++++++------------ 8 files changed, 53 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index 81f23e8..82e1066 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -16,7 +16,6 @@ import androidx.recyclerview.widget.RecyclerView import com.chargebee.android.Chargebee import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBPurchase -import com.chargebee.android.billingservice.RestorePurchaseCallback import com.chargebee.android.models.CBRestoreSubscription import com.chargebee.android.exceptions.CBException import com.chargebee.android.models.CBProduct @@ -212,12 +211,12 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { private fun restorePurchases() { showProgressDialog() CBPurchase.restorePurchases( - context = this, inActivePurchases = true, - completionCallback = object : RestorePurchaseCallback { + context = this, includeInActivePurchases = false, + completionCallback = object : CBCallback.RestorePurchaseCallback { override fun onSuccess(result: List) { hideProgressDialog() result.forEach { - Log.i(javaClass.simpleName, "status : ${it.store_status}") + Log.i(javaClass.simpleName, "status : ${it.storeStatus}") } CoroutineScope(Dispatchers.Main).launch { if (result.isNotEmpty()) 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 9d4a234..21af7a8 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -210,7 +210,7 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene * * @param [completionCallback] The listener will be called when restore purchase completes. */ - internal fun restorePurchases(completionCallback: RestorePurchaseCallback) { + internal fun restorePurchases(completionCallback: CBCallback.RestorePurchaseCallback) { queryPurchaseHistoryFromStore(completionCallback) } @@ -458,7 +458,7 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene } } - private fun queryPurchaseHistoryFromStore(completionCallback: RestorePurchaseCallback) { + private fun queryPurchaseHistoryFromStore(completionCallback: CBCallback.RestorePurchaseCallback) { onConnected({ status -> if (status) queryPurchaseHistory({ purchaseHistoryList -> val storeTransactions = arrayListOf() 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 975a8e9..ff78e22 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBCallback.kt @@ -9,17 +9,19 @@ interface CBCallback { fun onSuccess(productIDs: ArrayList) fun onError(error: CBException) } + interface ListProductsCallback { fun onSuccess(productIDs: ArrayList) fun onError(error: CBException) } + interface PurchaseCallback { fun onSuccess(result: ReceiptDetail, status: Boolean) fun onError(error: CBException) } -} -interface RestorePurchaseCallback { - fun onSuccess(result: List) - fun onError(error: CBException) + interface RestorePurchaseCallback { + fun onSuccess(result: List) + 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 3e4f918..7225a83 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -16,12 +16,12 @@ import com.chargebee.android.resources.ReceiptResource object CBPurchase { - var billingClientManager: BillingClientManager? = null + private var billingClientManager: BillingClientManager? = null val productIdList = arrayListOf() private var customer : CBCustomer? = null - var inActivePurchases = false + internal var includeInActivePurchases = false - enum class ProductType(val value: String) { + internal enum class ProductType(val value: String) { SUBS("subs"), INAPP("inapp") } @@ -104,16 +104,16 @@ object CBPurchase { /** * This method will provide all the purchases associated with the current account based on the [inActivePurchases] flag set. - * And the associated purchases can be synced with Chargebee. + * And the associated purchases will be synced with Chargebee. * * @param [context] Current activity context * @param [inActivePurchases] False by default. if true, only active purchases restores and synced with Chargebee. * @param [completionCallback] The listener will be called when restore purchase completes. */ @JvmStatic - fun restorePurchases(context: Context, inActivePurchases: Boolean = false, completionCallback: RestorePurchaseCallback){ - this.inActivePurchases = inActivePurchases - shareInstance(context).restorePurchases(completionCallback) + fun restorePurchases(context: Context, includeInActivePurchases: Boolean = false, completionCallback: CBCallback.RestorePurchaseCallback){ + this.includeInActivePurchases = includeInActivePurchases + sharedInstance(context).restorePurchases(completionCallback) } /* Chargebee Method - used to validate the receipt of purchase */ @JvmStatic @@ -132,7 +132,7 @@ object CBPurchase { } } - fun validateReceipt(purchaseToken: String, productId: String, completion : (ChargebeeResult) -> Unit){ + internal fun validateReceipt(purchaseToken: String, productId: String, completion : (ChargebeeResult) -> Unit){ val logger = CBLogger(name = "buy", action = "process_purchase_command") val params = Params( purchaseToken, @@ -231,7 +231,7 @@ object CBPurchase { if (arr.size==1) list.add("Standard") return list.toTypedArray() } - private fun shareInstance(context: Context): BillingClientManager { + private fun sharedInstance(context: Context): BillingClientManager { if (billingClientManager == null) { billingClientManager = BillingClientManager(context) } diff --git a/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt index 679a313..e6154af 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt @@ -1,9 +1,22 @@ package com.chargebee.android.models -data class CBRestoreSubscription(val subscription_id: String, val plan_id: String, val store_status: StoreStatus) -data class CBRestorePurchases(val in_app_subscriptions: ArrayList) +import com.google.gson.annotations.SerializedName -enum class StoreStatus{ +data class CBRestoreSubscription( + @SerializedName("subscription_id") + val subscriptionId: String, + @SerializedName("plan_id") + val planId: String, + @SerializedName("store_status") + val storeStatus: StoreStatus +) + +data class CBRestorePurchases( + @SerializedName("in_app_subscriptions") + val inAppSubscriptions: ArrayList +) + +enum class StoreStatus { active, in_trial, cancelled, diff --git a/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt b/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt index ee84ea4..ab999c4 100644 --- a/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt +++ b/chargebee/src/main/java/com/chargebee/android/repository/PurchaseRepository.kt @@ -41,7 +41,7 @@ internal interface PurchaseRepository { @FormUrlEncoded @POST("v2/in_app_subscriptions/{sdkKey}/retrieve") - suspend fun retrieveRestoreSubscription( + suspend fun restoreSubscription( @Header("Authorization") token: String = Chargebee.encodedApiKey, @Header("platform") platform: String = Chargebee.platform, @Header("version") sdkVersion: String = Chargebee.sdkVersion, diff --git a/chargebee/src/main/java/com/chargebee/android/resources/RestorePurchaseResource.kt b/chargebee/src/main/java/com/chargebee/android/resources/RestorePurchaseResource.kt index b3ce0ab..10353ae 100644 --- a/chargebee/src/main/java/com/chargebee/android/resources/RestorePurchaseResource.kt +++ b/chargebee/src/main/java/com/chargebee/android/resources/RestorePurchaseResource.kt @@ -10,7 +10,7 @@ internal class RestorePurchaseResource : BaseResource(Chargebee.baseUrl) { internal suspend fun retrieveStoreSubscription(purchaseToken: String): ChargebeeResult { val dataMap = convertToMap(purchaseToken) val response = apiClient.create(PurchaseRepository::class.java) - .retrieveRestoreSubscription(data = dataMap) + .restoreSubscription(data = dataMap) return responseFromServer( response ) 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 5d10356..96fec6b 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -1,11 +1,10 @@ package com.chargebee.android.restore import android.util.Log -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.GPErrorCode -import com.chargebee.android.billingservice.RestorePurchaseCallback import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.loggers.CBLogger @@ -20,28 +19,18 @@ class CBRestorePurchaseManager { private var allTransactions = ArrayList() private var restorePurchases = ArrayList() private var activeTransactions = ArrayList() - lateinit var completionCallback: RestorePurchaseCallback + private lateinit var completionCallback: CBCallback.RestorePurchaseCallback private fun retrieveStoreSubscription( purchaseToken: String, completion: (ChargebeeResult) -> Unit ) { - try { - val logger = CBLogger(name = "restore", action = "retrieve_restore_subscriptions") - ResultHandler.safeExecuter( - { RestorePurchaseResource().retrieveStoreSubscription(purchaseToken) }, - completion, - logger - ) - } catch (exp: Exception) { - ChargebeeResult.Error( - exp = CBException( - error = ErrorDetail( - exp.message - ) - ) - ) - } + val logger = CBLogger(name = "restore", action = "retrieve_restore_subscriptions") + ResultHandler.safeExecuter( + { RestorePurchaseResource().retrieveStoreSubscription(purchaseToken) }, + completion, + logger + ) } internal fun retrieveRestoreSubscription( @@ -53,7 +42,7 @@ class CBRestorePurchaseManager { when (it) { is ChargebeeResult.Success -> { val restoreSubscription = - ((it.data) as CBRestorePurchases).in_app_subscriptions.firstOrNull() + ((it.data) as CBRestorePurchases).inAppSubscriptions.firstOrNull() restoreSubscription?.let { result(restoreSubscription) } @@ -67,7 +56,7 @@ class CBRestorePurchaseManager { internal fun fetchStoreSubscriptionStatus( storeTransactions: ArrayList, - completionCallback: RestorePurchaseCallback + completionCallback: CBCallback.RestorePurchaseCallback ) { this.completionCallback = completionCallback val storeTransaction = @@ -75,7 +64,7 @@ class CBRestorePurchaseManager { storeTransaction?.purchaseToken?.let { purchaseToken -> retrieveRestoreSubscription(purchaseToken, { restorePurchases.add(it) - when (it.store_status) { + when (it.storeStatus) { StoreStatus.active -> activeTransactions.add(storeTransaction) else -> allTransactions.add(storeTransaction) } @@ -99,13 +88,13 @@ class CBRestorePurchaseManager { ) } else { val activePurchases = restorePurchases.filter { subscription -> - subscription.store_status == StoreStatus.active + subscription.storeStatus == StoreStatus.active } val allPurchases = restorePurchases.filter { subscription -> - subscription.store_status == StoreStatus.active || subscription.store_status == StoreStatus.in_trial - || subscription.store_status == StoreStatus.cancelled || subscription.store_status == StoreStatus.paused + subscription.storeStatus == StoreStatus.active || subscription.storeStatus == StoreStatus.in_trial + || subscription.storeStatus == StoreStatus.cancelled || subscription.storeStatus == StoreStatus.paused } - if (CBPurchase.inActivePurchases) { + if (CBPurchase.includeInActivePurchases) { completionCallback.onSuccess(activePurchases) syncPurchaseWithChargebee(activeTransactions) } else { From 55d788ed2038550d2707f401c19e2b8be2264e6a Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 4 May 2023 16:58:41 +0530 Subject: [PATCH 06/84] Addressed review comments --- .../billingservice/BillingClientManager.kt | 137 +++++++++++------- .../restore/CBRestorePurchaseManager.kt | 33 +++-- 2 files changed, 104 insertions(+), 66 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 21af7a8..63e140b 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -30,6 +30,7 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene private val skusWithSkuDetails = arrayListOf() private val TAG = javaClass.simpleName lateinit var product: CBProduct + private lateinit var completionCallback: CBCallback.RestorePurchaseCallback constructor( context: Context, skuType: String, @@ -42,6 +43,7 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene startBillingServiceConnection() } + constructor(context: Context) { this.mContext = context } @@ -205,13 +207,18 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene } /** - * This method will provide all the purchases associated with the current account based on the [inActivePurchases] flag set. - * And the associated purchases can be synced with Chargebee. + * This method will provide all the purchases associated with the current account based on the [includeInActivePurchases] flag set. + * And the associated purchases will be synced with Chargebee. * * @param [completionCallback] The listener will be called when restore purchase completes. */ internal fun restorePurchases(completionCallback: CBCallback.RestorePurchaseCallback) { - queryPurchaseHistoryFromStore(completionCallback) + this.completionCallback = completionCallback + onConnected({ status -> + queryPurchaseHistoryFromStore(status) + }, { error -> + completionCallback.onError(error) + }) } /* Checks if the specified feature is supported by the Play Store */ @@ -458,38 +465,63 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene } } - private fun queryPurchaseHistoryFromStore(completionCallback: CBCallback.RestorePurchaseCallback) { - onConnected({ status -> - if (status) queryPurchaseHistory({ purchaseHistoryList -> + private val connectionError = CBException( + ErrorDetail( + message = RestoreErrorCode.SERVICE_UNAVAILABLE.name, + httpStatusCode = RestoreErrorCode.SERVICE_UNAVAILABLE.code + ) + ) + + private fun queryPurchaseHistoryFromStore( + connectionStatus: Boolean + ) { + if (connectionStatus) { + queryPurchaseHistory { purchaseHistoryList -> val storeTransactions = arrayListOf() storeTransactions.addAll(purchaseHistoryList) - CBRestorePurchaseManager.fetchStoreSubscriptionStatus(storeTransactions, completionCallback) - }, { error -> completionCallback.onError(error) }) - }, { error -> - completionCallback.onError(error) - }) + CBRestorePurchaseManager.fetchStoreSubscriptionStatus( + storeTransactions, + completionCallback + ) + } + } else { + completionCallback.onError( + connectionError + ) + } } private fun queryPurchaseHistory( - completionCallback: (List) -> Unit, - connectionError: (CBException) -> Unit + storeTransactions: (List) -> Unit ) { - queryAllPurchaseHistory(CBPurchase.ProductType.SUBS.value, { subscriptionTransactionList -> - queryAllPurchaseHistory( - CBPurchase.ProductType.INAPP.value, - { inAppPurchaseHistoryList -> - val purchaseTransactionHistory = inAppPurchaseHistoryList?.let { - subscriptionTransactionList?.plus(it) - } - completionCallback(purchaseTransactionHistory ?: emptyList()) - }, - { purchaseError -> connectionError(purchaseError) }) - }, { purchaseError -> connectionError(purchaseError) }) + queryAllSubsPurchaseHistory(CBPurchase.ProductType.SUBS.value) { subscriptionHistory -> + queryAllInAppPurchaseHistory(CBPurchase.ProductType.INAPP.value) { inAppHistory -> + val purchaseTransactionHistory = inAppHistory?.let { + subscriptionHistory?.plus(it) + } + storeTransactions(purchaseTransactionHistory ?: emptyList()) + } + } + } + + private fun queryAllSubsPurchaseHistory( + productType: String, purchaseTransactionList: (List?) -> Unit + ) { + queryPurchaseHistoryAsync(productType) { + purchaseTransactionList(it) + } + } + + private fun queryAllInAppPurchaseHistory( + productType: String, purchaseTransactionList: (List?) -> Unit + ) { + queryPurchaseHistoryAsync(productType) { + purchaseTransactionList(it) + } } - private fun queryAllPurchaseHistory( - productType: String, purchaseTransactionList: (List?) -> Unit, - purchaseError: (CBException) -> Unit + private fun queryPurchaseHistoryAsync( + productType: String, purchaseTransactionList: (List?) -> Unit ) { billingClient?.queryPurchaseHistoryAsync(productType) { billingResult, subsHistoryList -> if (billingResult.responseCode == OK) { @@ -498,7 +530,7 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene } purchaseTransactionList(purchaseHistoryList) } else { - purchaseError(throwCBException(billingResult)) + completionCallback.onError(throwCBException(billingResult)) } } } @@ -526,30 +558,33 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene val billingClient = buildBillingClient(this) if (billingClient?.isReady == false) { handler.postDelayed({ - billingClient.startConnection(object : - BillingClientStateListener { - override fun onBillingServiceDisconnected() { - Log.i(javaClass.simpleName, "onBillingServiceDisconnected") - status(false) - } - - override fun onBillingSetupFinished(billingResult: BillingResult) { - when (billingResult.responseCode) { - OK -> { - Log.i( - TAG, - "Google Billing Setup Done!" - ) - status(true) - } - else -> { - connectionError(throwCBException(billingResult)) - } - } - - } - }) + billingClient.startConnection( + createBillingClientStateListener(status, connectionError) + ) }, CONNECT_TIMER_START_MILLISECONDS) } else status(true) } + + private fun createBillingClientStateListener( + status: (Boolean) -> Unit, + connectionError: (CBException) -> Unit + ) = object : + BillingClientStateListener { + override fun onBillingServiceDisconnected() { + Log.i(javaClass.simpleName, "onBillingServiceDisconnected") + status(false) + } + + override fun onBillingSetupFinished(billingResult: BillingResult) { + when (billingResult.responseCode) { + OK -> { + Log.i(TAG, "Google Billing Setup Done!") + status(true) + } + else -> { + connectionError(throwCBException(billingResult)) + } + } + } + } } 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 96fec6b..48c190a 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -12,7 +12,6 @@ import com.chargebee.android.models.* import com.chargebee.android.models.ResultHandler import com.chargebee.android.resources.RestorePurchaseResource - class CBRestorePurchaseManager { companion object { @@ -59,25 +58,29 @@ class CBRestorePurchaseManager { completionCallback: CBCallback.RestorePurchaseCallback ) { this.completionCallback = completionCallback - val storeTransaction = - storeTransactions.firstOrNull()?.also { storeTransactions.remove(it) } - storeTransaction?.purchaseToken?.let { purchaseToken -> - retrieveRestoreSubscription(purchaseToken, { - restorePurchases.add(it) - when (it.storeStatus) { - StoreStatus.active -> activeTransactions.add(storeTransaction) - else -> allTransactions.add(storeTransaction) - } - getRestorePurchases(storeTransactions) - }, { _ -> - getRestorePurchases(storeTransactions) - }) + if (storeTransactions.isNotEmpty()) { + val storeTransaction = + storeTransactions.firstOrNull()?.also { storeTransactions.remove(it) } + storeTransaction?.purchaseToken?.let { purchaseToken -> + retrieveRestoreSubscription(purchaseToken, { + restorePurchases.add(it) + when (it.storeStatus) { + StoreStatus.active -> activeTransactions.add(storeTransaction) + else -> allTransactions.add(storeTransaction) + } + getRestorePurchases(storeTransactions) + }, { _ -> + getRestorePurchases(storeTransactions) + }) + } + } else { + completionCallback.onSuccess(emptyList()) } } internal fun getRestorePurchases(storeTransactions: ArrayList) { if (storeTransactions.isEmpty()) { - if(restorePurchases.isEmpty()) { + if (restorePurchases.isEmpty()) { completionCallback.onError( CBException( ErrorDetail( From 0106a81414c410dcee5052ba7a4674b594f94ae8 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Fri, 5 May 2023 10:56:04 +0530 Subject: [PATCH 07/84] Refactor: retrieveProducts and purchase product method on billing client manager And addressed review comments --- .../billingservice/BillingClientManager.kt | 242 +++--------------- .../android/billingservice/CBPurchase.kt | 167 +++++++----- .../android/billingservice/GPErrorCode.kt | 41 +-- .../android/models/CBRestoreSubscription.kt | 12 +- .../restore/CBRestorePurchaseManager.kt | 8 +- 5 files changed, 180 insertions(+), 290 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 63e140b..b9fdb5b 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -8,7 +8,7 @@ import android.util.Log import com.android.billingclient.api.* import com.android.billingclient.api.BillingClient.BillingResponseCode.* import com.chargebee.android.ErrorDetail -import com.chargebee.android.billingservice.RestoreErrorCode.Companion.throwCBException +import com.chargebee.android.billingservice.BillingErrorCode.Companion.throwCBException import com.chargebee.android.models.PurchaseTransaction import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult @@ -17,11 +17,11 @@ import com.chargebee.android.network.CBReceiptResponse import com.chargebee.android.restore.CBRestorePurchaseManager import kotlin.collections.ArrayList -class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListener { +class BillingClientManager(context: Context) : PurchasesUpdatedListener { private val CONNECT_TIMER_START_MILLISECONDS = 1L * 1000L - internal var billingClient: BillingClient? = null - var mContext: Context? = null + private var billingClient: BillingClient? = null + var mContext: Context? = context private val handler = Handler(Looper.getMainLooper()) private var skuType: String? = null private var skuList = arrayListOf() @@ -32,91 +32,25 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene lateinit var product: CBProduct private lateinit var completionCallback: CBCallback.RestorePurchaseCallback - constructor( - context: Context, skuType: String, - skuList: ArrayList, callBack: CBCallback.ListProductsCallback> - ) { - mContext = context - this.skuList = skuList - this.skuType = skuType - this.callBack = callBack - startBillingServiceConnection() - - } - - constructor(context: Context) { + init { this.mContext = context } - /* Called to notify that the connection to the billing service was lost*/ - override fun onBillingServiceDisconnected() { - connectToBillingService() - } - - /* The listener method will be called when the billing client setup process complete */ - override fun onBillingSetupFinished(billingResult: BillingResult) { - when (billingResult.responseCode) { - OK -> { - Log.i( - TAG, - "Google Billing Setup Done!" - ) - loadProductDetails(BillingClient.SkuType.SUBS, skuList, callBack) - } - FEATURE_NOT_SUPPORTED, - BILLING_UNAVAILABLE -> { + internal fun retrieveProducts( + @BillingClient.SkuType skuType: String, + skuList: ArrayList, callBack: CBCallback.ListProductsCallback> + ) { + onConnected({ status -> + if (status) + loadProductDetails(skuType, skuList, callBack) + else callBack.onError( - CBException( - ErrorDetail( - message = GPErrorCode.BillingUnavailable.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - Log.i(TAG, "onBillingSetupFinished() -> with error: ${billingResult.debugMessage}") - } - SERVICE_DISCONNECTED, - USER_CANCELED, - SERVICE_UNAVAILABLE, - ITEM_UNAVAILABLE, - ERROR, - ITEM_ALREADY_OWNED, - SERVICE_TIMEOUT, - ITEM_NOT_OWNED -> { - Log.i( - TAG, - "onBillingSetupFinished() -> google billing client error: ${billingResult.debugMessage}" - ) - } - DEVELOPER_ERROR -> { - Log.i( - TAG, - "onBillingSetupFinished() -> Client is already in the process of connecting to billing service" + connectionError ) - - } - else -> { - Log.i(TAG, "onBillingSetupFinished -> with error: ${billingResult.debugMessage}.") - } - } - } - - /* Method used to configure and create a instance of billing client */ - private fun startBillingServiceConnection() { - buildBillingClient(this) - connectToBillingService() - } - - /* Connect the billing client service */ - private fun connectToBillingService() { - if (billingClient?.isReady == false) { - handler.postDelayed( - { billingClient?.startConnection(this@BillingClientManager) }, - CONNECT_TIMER_START_MILLISECONDS - ) - } + }, { error -> + callBack.onError(error) + }) } - /* Get the SKU/Products from Play Console */ private fun loadProductDetails( @BillingClient.SkuType skuType: String, @@ -174,15 +108,27 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene Log.e(TAG, "exception :$exp.message") callBack.onError(CBException(ErrorDetail(message = "${exp.message}"))) } - } - /* Purchase the product: Initiates the billing flow for an In-app-purchase */ - fun purchase( + internal fun purchase( product: CBProduct, purchaseCallBack: CBCallback.PurchaseCallback ) { this.purchaseCallBack = purchaseCallBack + onConnected({ status -> + if (status) + purchase(product) + else + purchaseCallBack.onError( + connectionError + ) + }, { error -> + purchaseCallBack.onError(error) + }) + + } + /* Purchase the product: Initiates the billing flow for an In-app-purchase */ + private fun purchase(product: CBProduct) { this.product = product val skuDetails = product.skuDetails @@ -195,7 +141,7 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene billingResult?.responseCode != OK }?.let { billingResult -> Log.e(TAG, "Failed to launch billing flow $billingResult") - purchaseCallBack.onError( + purchaseCallBack?.onError( CBException( ErrorDetail( message = GPErrorCode.LaunchBillingFlowError.errorMsg, @@ -280,119 +226,11 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene } } } - ITEM_ALREADY_OWNED -> { - Log.e(TAG, "Billing response code : ITEM_ALREADY_OWNED") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.ProductAlreadyOwned.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - SERVICE_DISCONNECTED -> { - connectToBillingService() - } - ITEM_UNAVAILABLE -> { - Log.e(TAG, "Billing response code : ITEM_UNAVAILABLE") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.ProductUnavailable.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - USER_CANCELED -> { - Log.e(TAG, "Billing response code : USER_CANCELED ") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.CanceledPurchase.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - ITEM_NOT_OWNED -> { - Log.e(TAG, "Billing response code : ITEM_NOT_OWNED ") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.ProductNotOwned.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - SERVICE_TIMEOUT -> { - Log.e(TAG, "Billing response code :SERVICE_TIMEOUT ") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.PlayServiceTimeOut.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - SERVICE_UNAVAILABLE -> { - Log.e(TAG, "Billing response code: SERVICE_UNAVAILABLE") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.PlayServiceUnavailable.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - ERROR -> { - Log.e(TAG, "Billing response code: ERROR") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.UnknownError.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - DEVELOPER_ERROR -> { - Log.e(TAG, "Billing response code: DEVELOPER_ERROR") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.DeveloperError.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - BILLING_UNAVAILABLE -> { - Log.e(TAG, "Billing response code: BILLING_UNAVAILABLE") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.BillingUnavailable.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } - FEATURE_NOT_SUPPORTED -> { - Log.e(TAG, "Billing response code: FEATURE_NOT_SUPPORTED") - purchaseCallBack?.onError( - CBException( - ErrorDetail( - message = GPErrorCode.FeatureNotSupported.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - ) - } + else -> { + purchaseCallBack?.onError( + throwCBException(billingResult) + ) + } } } @@ -467,8 +305,8 @@ class BillingClientManager : BillingClientStateListener, PurchasesUpdatedListene private val connectionError = CBException( ErrorDetail( - message = RestoreErrorCode.SERVICE_UNAVAILABLE.name, - httpStatusCode = RestoreErrorCode.SERVICE_UNAVAILABLE.code + message = BillingErrorCode.billingDebugMessage(BillingErrorCode.SERVICE_UNAVAILABLE.code), + httpStatusCode = BillingErrorCode.SERVICE_UNAVAILABLE.code ) ) 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 7225a83..317c366 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -18,7 +18,7 @@ object CBPurchase { private var billingClientManager: BillingClientManager? = null val productIdList = arrayListOf() - private var customer : CBCustomer? = null + private var customer: CBCustomer? = null internal var includeInActivePurchases = false internal enum class ProductType(val value: String) { @@ -26,60 +26,74 @@ object CBPurchase { INAPP("inapp") } - annotation class SkuType { - companion object { - var INAPP = "inapp" - var SUBS = "subs" - } - } /* * Get the product ID's from chargebee system */ @JvmStatic - fun retrieveProductIdentifers(params: Array = arrayOf(), completion : (CBProductIDResult>) -> Unit) { + fun retrieveProductIdentifers( + params: Array = arrayOf(), + completion: (CBProductIDResult>) -> Unit + ) { if (params.isNotEmpty()) { params[0] = params[0].ifEmpty { Chargebee.limit } val queryParams = append(params) retrieveProductIDList(queryParams, completion) - }else{retrieveProductIDList(arrayOf(), completion) } + } else { + retrieveProductIDList(arrayOf(), completion) + } } - /* Get the product/sku details from Play console */ + + /** + * Get the CBProducts for the given list of product Ids + * @param [context] current activity context + * @param [params] list of product Ids + * @param [callBack] The listener will be called when retrieve products completes. + */ @JvmStatic - fun retrieveProducts(context: Context, params: ArrayList, callBack : CBCallback.ListProductsCallback>) { - try { - val connectionState = billingClientManager?.billingClient?.connectionState - if (connectionState!=null && connectionState == BillingClient.ConnectionState.CONNECTED){ - billingClientManager?.billingClient?.endConnection() - } - billingClientManager = BillingClientManager(context,SkuType.SUBS, params, callBack) - }catch (ex: CBException){ - callBack.onError(ex) - } + fun retrieveProducts( + context: Context, + params: ArrayList, + callBack: CBCallback.ListProductsCallback> + ) { + sharedInstance(context).retrieveProducts(ProductType.SUBS.value, params, callBack) } - /* Buy the product with/without customer Id */ - @Deprecated(message = "This will be removed in upcoming release, Please use API fun - purchaseProduct(product: CBProduct, customer : CBCustomer? = null, callback)", level = DeprecationLevel.WARNING) + /** + * Buy the product with/without customer id + * @param [product] The product that wish to purchase + * @param [callback] listener will be called when product purchase completes. + */ + @Deprecated( + message = "This will be removed in upcoming release, Please use API fun - purchaseProduct(product: CBProduct, customer : CBCustomer? = null, callback)", + level = DeprecationLevel.WARNING + ) @JvmStatic fun purchaseProduct( product: CBProduct, customerID: String, - callback: CBCallback.PurchaseCallback) { - customer = CBCustomer(customerID,"","","") + callback: CBCallback.PurchaseCallback + ) { + customer = CBCustomer(customerID, "", "", "") purchaseProduct(product, callback) } - /* Buy the product with/without customer info */ + /** + * Buy the product with/without customer data + * @param [product] The product that wish to purchase + * @param [callback] listener will be called when product purchase completes. + */ @JvmStatic fun purchaseProduct( product: CBProduct, customer: CBCustomer? = null, - callback: CBCallback.PurchaseCallback) { + callback: CBCallback.PurchaseCallback + ) { this.customer = customer purchaseProduct(product, callback) } - private fun purchaseProduct(product: CBProduct,callback: CBCallback.PurchaseCallback){ - if (!TextUtils.isEmpty(Chargebee.sdkKey)){ - CBAuthentication.isSDKKeyValid(Chargebee.sdkKey){ - when(it){ + private fun purchaseProduct(product: CBProduct, callback: CBCallback.PurchaseCallback) { + if (!TextUtils.isEmpty(Chargebee.sdkKey)) { + CBAuthentication.isSDKKeyValid(Chargebee.sdkKey) { + when (it) { is ChargebeeResult.Success -> { if (billingClientManager?.isFeatureSupported() == true) { if (billingClientManager?.isBillingClientReady() == true) { @@ -87,41 +101,57 @@ object CBPurchase { } else { callback.onError(CBException(ErrorDetail(GPErrorCode.BillingClientNotReady.errorMsg))) } - }else { + } else { callback.onError(CBException(ErrorDetail(GPErrorCode.FeatureNotSupported.errorMsg))) } } - is ChargebeeResult.Error ->{ + is ChargebeeResult.Error -> { Log.i(javaClass.simpleName, "Exception from server :${it.exp.message}") callback.onError(it.exp) } } } - }else{ - callback.onError(CBException(ErrorDetail(message = GPErrorCode.SDKKeyNotAvailable.errorMsg, httpStatusCode = 400))) + } else { + callback.onError( + CBException( + ErrorDetail( + message = GPErrorCode.SDKKeyNotAvailable.errorMsg, + httpStatusCode = 400 + ) + ) + ) } } /** - * This method will provide all the purchases associated with the current account based on the [inActivePurchases] flag set. + * This method will provide all the purchases associated with the current account based on the [includeInActivePurchases] flag set. * And the associated purchases will be synced with Chargebee. * * @param [context] Current activity context - * @param [inActivePurchases] False by default. if true, only active purchases restores and synced with Chargebee. + * @param [includeInActivePurchases] False by default. if true, only active purchases restores and synced with Chargebee. * @param [completionCallback] The listener will be called when restore purchase completes. */ @JvmStatic - fun restorePurchases(context: Context, includeInActivePurchases: Boolean = false, completionCallback: CBCallback.RestorePurchaseCallback){ + fun restorePurchases( + context: Context, + includeInActivePurchases: Boolean = false, + completionCallback: CBCallback.RestorePurchaseCallback + ) { this.includeInActivePurchases = includeInActivePurchases sharedInstance(context).restorePurchases(completionCallback) } + /* Chargebee Method - used to validate the receipt of purchase */ @JvmStatic - fun validateReceipt(purchaseToken: String, product: CBProduct, completion : (ChargebeeResult) -> Unit) { + fun validateReceipt( + purchaseToken: String, + product: CBProduct, + completion: (ChargebeeResult) -> Unit + ) { try { validateReceipt(purchaseToken, product.productId, completion) - }catch (exp: Exception){ - Log.e(javaClass.simpleName, "Exception in validateReceipt() :"+exp.message) + } catch (exp: Exception) { + Log.e(javaClass.simpleName, "Exception in validateReceipt() :" + exp.message) ChargebeeResult.Error( exp = CBException( error = ErrorDetail( @@ -132,7 +162,11 @@ object CBPurchase { } } - internal fun validateReceipt(purchaseToken: String, productId: String, completion : (ChargebeeResult) -> Unit){ + internal fun validateReceipt( + purchaseToken: String, + productId: String, + completion: (ChargebeeResult) -> Unit + ) { val logger = CBLogger(name = "buy", action = "process_purchase_command") val params = Params( purchaseToken, @@ -140,7 +174,6 @@ object CBPurchase { customer, Chargebee.channel ) - ResultHandler.safeExecuter( { ReceiptResource().validateReceipt(params) }, completion, @@ -151,18 +184,21 @@ object CBPurchase { /* * Get the product ID's from chargebee system. */ - fun retrieveProductIDList(params: Array, completion: (CBProductIDResult>) -> Unit){ + private fun retrieveProductIDList( + params: Array, + completion: (CBProductIDResult>) -> Unit + ) { // The Plan will be fetched based on the user catalog versions in chargebee system. - when(Chargebee.version){ + when (Chargebee.version) { // If user catalog version1 then get the plan's - CatalogVersion.V1.value ->{ - Chargebee.retrieveAllPlans(params){ + CatalogVersion.V1.value -> { + Chargebee.retrieveAllPlans(params) { when (it) { is ChargebeeResult.Success -> { Log.i(javaClass.simpleName, "list plan ID's : ${it.data}") val productsList = (it.data as PlansWrapper).list productIdList.clear() - for (plan in productsList){ + for (plan in productsList) { if (!TextUtils.isEmpty(plan.plan.channel)) { val id = plan.plan.id.split("-") productIdList.add(id[0]) @@ -172,65 +208,74 @@ object CBPurchase { completion(CBProductIDResult.ProductIds(productIdList)) } is ChargebeeResult.Error -> { - Log.e(javaClass.simpleName, "Error retrieving all plans : ${it.exp.message}") + Log.e( + javaClass.simpleName, + "Error retrieving all plans : ${it.exp.message}" + ) completion(CBProductIDResult.Error(CBException(ErrorDetail(it.exp.message)))) } } } } // If user catalog version2 then get the Item's - CatalogVersion.V2.value ->{ - Chargebee.retrieveAllItems(params){ + CatalogVersion.V2.value -> { + Chargebee.retrieveAllItems(params) { when (it) { is ChargebeeResult.Success -> { Log.i(javaClass.simpleName, "list item ID's : ${it.data}") val productsList = (it.data as ItemsWrapper).list productIdList.clear() - for (item in productsList){ + for (item in productsList) { productIdList.add(item.item.id) } completion(CBProductIDResult.ProductIds(productIdList)) } is ChargebeeResult.Error -> { - Log.e(javaClass.simpleName, "Error retrieving all items : ${it.exp.message}") + Log.e( + javaClass.simpleName, + "Error retrieving all items : ${it.exp.message}" + ) completion(CBProductIDResult.Error(CBException(ErrorDetail(it.exp.message)))) } } } } // Check the catalog version - CatalogVersion.Unknown.value ->{ - val auth = Auth(Chargebee.sdkKey, + CatalogVersion.Unknown.value -> { + val auth = Auth( + Chargebee.sdkKey, Chargebee.applicationId, Chargebee.appName, Chargebee.channel ) CBAuthentication.authenticate(auth) { - when(it){ - is ChargebeeResult.Success ->{ + when (it) { + is ChargebeeResult.Success -> { Log.i(javaClass.simpleName, " Response :${it.data}") val response = it.data as CBAuthResponse Chargebee.version = response.in_app_detail.product_catalog_version - retrieveProductIDList(params,completion) + retrieveProductIDList(params, completion) } - is ChargebeeResult.Error ->{ + is ChargebeeResult.Error -> { Log.i(javaClass.simpleName, "Invalid catalog version") completion(CBProductIDResult.Error(CBException(ErrorDetail("Invalid catalog version")))) } } } } - else ->{ + else -> { Log.i(javaClass.simpleName, "Unknown error") completion(CBProductIDResult.Error(CBException(ErrorDetail("Unknown error")))) } } } - fun append(arr: Array): Array { + + internal fun append(arr: Array): Array { val list: MutableList = arr.toMutableList() - if (arr.size==1) list.add("Standard") + if (arr.size == 1) list.add("Standard") return list.toTypedArray() } + private fun sharedInstance(context: Context): BillingClientManager { if (billingClientManager == null) { billingClientManager = BillingClientManager(context) diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt index f68af79..86c5999 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt @@ -23,10 +23,11 @@ enum class GPErrorCode(val errorMsg: String) { DeveloperError("Invalid arguments provided to the API"), BillingClientNotReady("Play services not available"), SDKKeyNotAvailable("SDK key not available to proceed purchase"), - InvalidPurchaseToken("The Token data sent is not correct or Google service is temporarily down") + InvalidPurchaseToken("The Token data sent is not correct or Google service is temporarily down"), + BillingServiceDisconnected("The app is not connected to Google play via Billing library") } -internal enum class RestoreErrorCode(val code: Int) { +internal enum class BillingErrorCode(val code: Int) { UNKNOWN(-4), SERVICE_TIMEOUT(-3), FEATURE_NOT_SUPPORTED(-2), @@ -36,10 +37,12 @@ internal enum class RestoreErrorCode(val code: Int) { ITEM_UNAVAILABLE(4), DEVELOPER_ERROR(5), ERROR(6), - ITEM_NOT_OWNED(8); + ITEM_NOT_OWNED(8), + SERVICE_DISCONNECTED(-1), + ITEM_ALREADY_OWNED(7); companion object { - private fun billingResponseCode(responseCode: Int): RestoreErrorCode = + private fun billingResponseCode(responseCode: Int): BillingErrorCode = when (responseCode) { BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> SERVICE_TIMEOUT BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> FEATURE_NOT_SUPPORTED @@ -50,32 +53,36 @@ internal enum class RestoreErrorCode(val code: Int) { BillingClient.BillingResponseCode.DEVELOPER_ERROR -> DEVELOPER_ERROR BillingClient.BillingResponseCode.ERROR -> ERROR BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> ITEM_NOT_OWNED + BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> SERVICE_DISCONNECTED + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> ITEM_ALREADY_OWNED else -> { UNKNOWN } } - private fun billingDebugMessage(responseCode: Int): GPErrorCode = + internal fun billingDebugMessage(responseCode: Int): String = when (responseCode) { - BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> GPErrorCode.PlayServiceTimeOut - BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> GPErrorCode.FeatureNotSupported - BillingClient.BillingResponseCode.USER_CANCELED -> GPErrorCode.CanceledPurchase - BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> GPErrorCode.PlayServiceUnavailable - BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> GPErrorCode.BillingUnavailable - BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> GPErrorCode.ProductUnavailable - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> GPErrorCode.DeveloperError - BillingClient.BillingResponseCode.ERROR -> GPErrorCode.UnknownError - BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> GPErrorCode.ProductNotOwned + BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> GPErrorCode.PlayServiceTimeOut.errorMsg + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> GPErrorCode.FeatureNotSupported.errorMsg + BillingClient.BillingResponseCode.USER_CANCELED -> GPErrorCode.CanceledPurchase.errorMsg + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> GPErrorCode.PlayServiceUnavailable.errorMsg + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> GPErrorCode.BillingUnavailable.errorMsg + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> GPErrorCode.ProductUnavailable.errorMsg + BillingClient.BillingResponseCode.DEVELOPER_ERROR -> GPErrorCode.DeveloperError.errorMsg + BillingClient.BillingResponseCode.ERROR -> GPErrorCode.UnknownError.errorMsg + BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> GPErrorCode.ProductNotOwned.errorMsg + BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> GPErrorCode.BillingServiceDisconnected.errorMsg + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> GPErrorCode.ProductAlreadyOwned.errorMsg else -> { - GPErrorCode.UnknownError + GPErrorCode.UnknownError.errorMsg } } - fun throwCBException(billingResult: BillingResult): CBException = + internal fun throwCBException(billingResult: BillingResult): CBException = CBException( ErrorDetail( httpStatusCode = billingResponseCode(billingResult.responseCode).code, - message = billingDebugMessage(billingResult.responseCode).errorMsg + message = billingDebugMessage(billingResult.responseCode) ) ) } diff --git a/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt index e6154af..2238a11 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/CBRestoreSubscription.kt @@ -8,7 +8,7 @@ data class CBRestoreSubscription( @SerializedName("plan_id") val planId: String, @SerializedName("store_status") - val storeStatus: StoreStatus + val storeStatus: String ) data class CBRestorePurchases( @@ -16,9 +16,9 @@ data class CBRestorePurchases( val inAppSubscriptions: ArrayList ) -enum class StoreStatus { - active, - in_trial, - cancelled, - paused +enum class StoreStatus(val value: String) { + Active("active"), + InTrial("in_trial"), + Cancelled("cancelled"), + Paused("paused") } \ 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 48c190a..23fb38f 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -65,7 +65,7 @@ class CBRestorePurchaseManager { retrieveRestoreSubscription(purchaseToken, { restorePurchases.add(it) when (it.storeStatus) { - StoreStatus.active -> activeTransactions.add(storeTransaction) + StoreStatus.Active.value -> activeTransactions.add(storeTransaction) else -> allTransactions.add(storeTransaction) } getRestorePurchases(storeTransactions) @@ -91,11 +91,11 @@ class CBRestorePurchaseManager { ) } else { val activePurchases = restorePurchases.filter { subscription -> - subscription.storeStatus == StoreStatus.active + subscription.storeStatus == StoreStatus.Active.value } val allPurchases = restorePurchases.filter { subscription -> - subscription.storeStatus == StoreStatus.active || subscription.storeStatus == StoreStatus.in_trial - || subscription.storeStatus == StoreStatus.cancelled || subscription.storeStatus == StoreStatus.paused + 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(activePurchases) From 11d80538f20637060ca9812a501030d33bf55ec2 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Fri, 5 May 2023 14:05:36 +0530 Subject: [PATCH 08/84] Updated test class --- .../java/com/chargebee/android/restore/RestorePurchaseTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7364da8..2111dca 100644 --- a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt @@ -3,7 +3,7 @@ package com.chargebee.android.restore import com.android.billingclient.api.* import com.chargebee.android.Chargebee import com.chargebee.android.ErrorDetail -import com.chargebee.android.billingservice.RestorePurchaseCallback +import com.chargebee.android.billingservice.CBCallback.RestorePurchaseCallback import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.* @@ -118,7 +118,7 @@ class RestorePurchaseTest { val purchaseTransaction = getTransaction(true) val cbRestorePurchasesList = arrayListOf() val purchaseToken = purchaseTransaction.first().purchaseToken - val cbRestoreSubscription = CBRestoreSubscription("", "", StoreStatus.active) + val cbRestoreSubscription = CBRestoreSubscription("", "", StoreStatus.Active.value) cbRestorePurchasesList.add(cbRestoreSubscription) CoroutineScope(Dispatchers.IO).launch { Mockito.`when`(RestorePurchaseResource().retrieveStoreSubscription(purchaseToken)) From 83433bfd0e109d0ab013b42038a4c4824658cf22 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 8 May 2023 13:35:22 +0530 Subject: [PATCH 09/84] If includeInActivePurchases set as true, restore all the purchases including active purchase else only active purchase gets restore. Updated test class --- .../java/com/chargebee/android/billingservice/CBPurchase.kt | 2 +- .../chargebee/android/restore/CBRestorePurchaseManager.kt | 6 +++--- .../android/billingservice/BillingClientManagerTest.kt | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) 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 317c366..3e756ff 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -184,7 +184,7 @@ object CBPurchase { /* * Get the product ID's from chargebee system. */ - private fun retrieveProductIDList( + internal fun retrieveProductIDList( params: Array, completion: (CBProductIDResult>) -> Unit ) { 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 23fb38f..1baced3 100644 --- a/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/restore/CBRestorePurchaseManager.kt @@ -98,11 +98,11 @@ class CBRestorePurchaseManager { || subscription.storeStatus == StoreStatus.Cancelled.value || subscription.storeStatus == StoreStatus.Paused.value } if (CBPurchase.includeInActivePurchases) { - completionCallback.onSuccess(activePurchases) - syncPurchaseWithChargebee(activeTransactions) - } else { completionCallback.onSuccess(allPurchases) syncPurchaseWithChargebee(allTransactions) + } else { + completionCallback.onSuccess(activePurchases) + syncPurchaseWithChargebee(activeTransactions) } } restorePurchases.clear() 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 03cf39c..427191d 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -64,9 +64,7 @@ class BillingClientManagerTest { billingClientManager = callBack?.let { BillingClientManager( - ApplicationProvider.getApplicationContext(), - BillingClient.SkuType.SUBS, - productIdList, it + ApplicationProvider.getApplicationContext() ) } } @@ -84,7 +82,7 @@ class BillingClientManagerTest { val productIdList = arrayListOf("merchant.pro.android", "merchant.premium.android") CoroutineScope(Dispatchers.IO).launch { - val skuType = CBPurchase.SkuType.SUBS + val skuType = CBPurchase.ProductType.SUBS Mockito.`when`(mContext?.let { CBPurchase.retrieveProducts( it, From e09274e525ddece69233adbac4b43f7e9c7249f5 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 9 May 2023 16:02:15 +0530 Subject: [PATCH 10/84] Improvements on error handling and updated README.md --- README.md | 36 ++++++ .../com/chargebee/example/MainActivity.kt | 3 +- .../billingservice/BillingClientManager.kt | 9 +- .../android/billingservice/GPErrorCode.kt | 115 ++++++++++-------- 4 files changed, 105 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 4842328..01a922c 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,42 @@ 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. +### 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. + +To retrieve **inactive** purchases along with the **active** purchases for your app user, you can call the `restorePurchases()` function with the `includeInActiveProducts` parameter set to `true`. If you only want to restore active subscriptions, set the parameter to `false`. Here is an example of how to use the `restorePurchases()` function in your code with the `includeInActiveProducts` parameter set to `true`. + +```kotlin +CBPurchase.restorePurchases(context = this, includeInActivePurchases = false, object : CBCallback.RestorePurchaseCallback{ + override fun onSuccess(result: List) { + result.forEach { + Log.i(javaClass.simpleName, "Successfully restored purchases") + } + } + override fun onError(error: CBException) { + Log.e(TAG, "Error: ${error.message}") + } +}) + ``` + +##### Return Subscriptions Object +The `restorePurchases()` function returns an array of subscription objects and each object holds three attributes `subscription_id`, `plan_id`, and `store_status`. The value of `store_status` can be used to verify the subscription status such as `Active`, `InTrial`, `Cancelled` and `Paused`. + +##### Error Handling +In the event of any failures while finding associated subscriptions for the restored items, The SDK will return an error, as mentioned in the following table. + +These are the possible error codes and their descriptions: +| Error Code | Description | +|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------| +| `BillingErrorCode.SERVICE_TIMEOUT` | The request has reached the maximum timeout before Google Play responds. | +| `BillingErrorCode.FEATURE_NOT_SUPPORTED` | The requested feature is not supported by the Play Store on the current device. | +| `BillingErrorCode.SERVICE_UNAVAILABLE` | The service is currently unavailable. | +| `BillingErrorCode.DEVELOPER_ERROR` | Error resulting from incorrect usage of the API. | +| `BillingErrorCode.ERROR` | Fatal error during the API action. | +| `BillingErrorCode.SERVICE_DISCONNECTED` | The app is not connected to the Play Store service via the Google Play Billing Library. | +| `BillingErrorCode.UNKNOWN` | Unknown error occurred. | + ### 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/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index 82e1066..d98d565 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -211,7 +211,7 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { private fun restorePurchases() { showProgressDialog() CBPurchase.restorePurchases( - context = this, includeInActivePurchases = false, + context = this, includeInActivePurchases = true, completionCallback = object : CBCallback.RestorePurchaseCallback { override fun onSuccess(result: List) { hideProgressDialog() @@ -223,7 +223,6 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { alertSuccess("${result.size} purchases restored successfully") else alertSuccess("Purchases not found to restore") - } } 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 b9fdb5b..bc4055b 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -95,12 +95,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } else { Log.e(TAG, "Response Code :" + billingResult.responseCode) callBack.onError( - CBException( - ErrorDetail( - message = GPErrorCode.PlayServiceUnavailable.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) + throwCBException(billingResult) ) } } @@ -305,7 +300,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private val connectionError = CBException( ErrorDetail( - message = BillingErrorCode.billingDebugMessage(BillingErrorCode.SERVICE_UNAVAILABLE.code), + message = BillingErrorCode.SERVICE_UNAVAILABLE.message, httpStatusCode = BillingErrorCode.SERVICE_UNAVAILABLE.code ) ) diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt index 86c5999..c643653 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/GPErrorCode.kt @@ -24,66 +24,83 @@ enum class GPErrorCode(val errorMsg: String) { BillingClientNotReady("Play services not available"), SDKKeyNotAvailable("SDK key not available to proceed purchase"), InvalidPurchaseToken("The Token data sent is not correct or Google service is temporarily down"), - BillingServiceDisconnected("The app is not connected to Google play via Billing library") } -internal enum class BillingErrorCode(val code: Int) { - UNKNOWN(-4), - SERVICE_TIMEOUT(-3), - FEATURE_NOT_SUPPORTED(-2), - USER_CANCELED(1), - SERVICE_UNAVAILABLE(2), - BILLING_UNAVAILABLE(3), - ITEM_UNAVAILABLE(4), - DEVELOPER_ERROR(5), - ERROR(6), - ITEM_NOT_OWNED(8), - SERVICE_DISCONNECTED(-1), - ITEM_ALREADY_OWNED(7); +internal enum class BillingErrorCode(val code: Int, val message: String) { + UNKNOWN(-4, "Unknown error occurred"), + SERVICE_TIMEOUT(-3, "The request has reached the maximum timeout before Google Play responds"), + FEATURE_NOT_SUPPORTED( + -2, + "The requested feature is not supported by the Play Store on the current device" + ), + USER_CANCELED(1, "Transaction was canceled by the user"), + SERVICE_UNAVAILABLE(2, "The service is currently unavailable"), + BILLING_UNAVAILABLE(3, "A user billing error occurred during processing"), + ITEM_UNAVAILABLE(4, "The requested product is not available for purchase"), + DEVELOPER_ERROR(5, "Error resulting from incorrect usage of the API"), + ERROR(6, "Fatal error during the API action"), + ITEM_NOT_OWNED(8, "Requested action on the item failed since it is not owned by the user"), + SERVICE_DISCONNECTED( + -1, + "The app is not connected to the Play Store service via the Google Play Billing Library" + ), + ITEM_ALREADY_OWNED(7, "The purchase failed because the item is already owned"); companion object { - private fun billingResponseCode(responseCode: Int): BillingErrorCode = + private fun errorDetail(responseCode: Int): ErrorDetail = when (responseCode) { - BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> SERVICE_TIMEOUT - BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> FEATURE_NOT_SUPPORTED - BillingClient.BillingResponseCode.USER_CANCELED -> USER_CANCELED - BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> SERVICE_UNAVAILABLE - BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> BILLING_UNAVAILABLE - BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> ITEM_UNAVAILABLE - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> DEVELOPER_ERROR - BillingClient.BillingResponseCode.ERROR -> ERROR - BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> ITEM_NOT_OWNED - BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> SERVICE_DISCONNECTED - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> ITEM_ALREADY_OWNED - else -> { - UNKNOWN - } - } - - internal fun billingDebugMessage(responseCode: Int): String = - when (responseCode) { - BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> GPErrorCode.PlayServiceTimeOut.errorMsg - BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> GPErrorCode.FeatureNotSupported.errorMsg - BillingClient.BillingResponseCode.USER_CANCELED -> GPErrorCode.CanceledPurchase.errorMsg - BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> GPErrorCode.PlayServiceUnavailable.errorMsg - BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> GPErrorCode.BillingUnavailable.errorMsg - BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> GPErrorCode.ProductUnavailable.errorMsg - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> GPErrorCode.DeveloperError.errorMsg - BillingClient.BillingResponseCode.ERROR -> GPErrorCode.UnknownError.errorMsg - BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> GPErrorCode.ProductNotOwned.errorMsg - BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> GPErrorCode.BillingServiceDisconnected.errorMsg - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> GPErrorCode.ProductAlreadyOwned.errorMsg + BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> ErrorDetail( + message = SERVICE_TIMEOUT.message, + httpStatusCode = SERVICE_TIMEOUT.code + ) + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> ErrorDetail( + message = FEATURE_NOT_SUPPORTED.message, + httpStatusCode = FEATURE_NOT_SUPPORTED.code + ) + BillingClient.BillingResponseCode.USER_CANCELED -> ErrorDetail( + message = USER_CANCELED.message, + httpStatusCode = USER_CANCELED.code + ) + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> ErrorDetail( + message = SERVICE_UNAVAILABLE.message, + httpStatusCode = SERVICE_UNAVAILABLE.code + ) + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> ErrorDetail( + message = BILLING_UNAVAILABLE.message, + httpStatusCode = BILLING_UNAVAILABLE.code + ) + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> ErrorDetail( + message = ITEM_UNAVAILABLE.message, + httpStatusCode = ITEM_UNAVAILABLE.code + ) + BillingClient.BillingResponseCode.DEVELOPER_ERROR -> ErrorDetail( + message = DEVELOPER_ERROR.message, + httpStatusCode = DEVELOPER_ERROR.code + ) + BillingClient.BillingResponseCode.ERROR -> ErrorDetail( + message = ERROR.message, + httpStatusCode = ERROR.code + ) + BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> ErrorDetail( + message = ITEM_NOT_OWNED.message, + httpStatusCode = ITEM_NOT_OWNED.code + ) + BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> ErrorDetail( + message = SERVICE_DISCONNECTED.message, + httpStatusCode = SERVICE_DISCONNECTED.code + ) + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> ErrorDetail( + message = ITEM_ALREADY_OWNED.message, + httpStatusCode = ITEM_ALREADY_OWNED.code + ) else -> { - GPErrorCode.UnknownError.errorMsg + ErrorDetail(message = UNKNOWN.message, httpStatusCode = UNKNOWN.code) } } internal fun throwCBException(billingResult: BillingResult): CBException = CBException( - ErrorDetail( - httpStatusCode = billingResponseCode(billingResult.responseCode).code, - message = billingDebugMessage(billingResult.responseCode) - ) + errorDetail(billingResult.responseCode) ) } } \ No newline at end of file From 8f930f2268abdbc093e5d53f73963f4e6e165831 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 11 May 2023 20:07:49 +0530 Subject: [PATCH 11/84] Removed unused property and updated README.md --- README.md | 2 +- .../android/billingservice/BillingClientManager.kt | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 01a922c..9aee583 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ The `restorePurchases()` function helps to recover your app user's previous purc To retrieve **inactive** purchases along with the **active** purchases for your app user, you can call the `restorePurchases()` function with the `includeInActiveProducts` parameter set to `true`. If you only want to restore active subscriptions, set the parameter to `false`. Here is an example of how to use the `restorePurchases()` function in your code with the `includeInActiveProducts` parameter set to `true`. ```kotlin -CBPurchase.restorePurchases(context = this, includeInActivePurchases = false, object : CBCallback.RestorePurchaseCallback{ +CBPurchase.restorePurchases(context = current activity context, includeInActivePurchases = false, object : CBCallback.RestorePurchaseCallback{ override fun onSuccess(result: List) { result.forEach { Log.i(javaClass.simpleName, "Successfully restored purchases") 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 bc4055b..c774fc3 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -23,14 +23,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private var billingClient: BillingClient? = null var mContext: Context? = context private val handler = Handler(Looper.getMainLooper()) - private var skuType: String? = null - private var skuList = arrayListOf() - private lateinit var callBack: CBCallback.ListProductsCallback> private var purchaseCallBack: CBCallback.PurchaseCallback? = null private val skusWithSkuDetails = arrayListOf() private val TAG = javaClass.simpleName lateinit var product: CBProduct - private lateinit var completionCallback: CBCallback.RestorePurchaseCallback + private lateinit var restorePurchaseCallBack: CBCallback.RestorePurchaseCallback init { this.mContext = context @@ -154,7 +151,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { * @param [completionCallback] The listener will be called when restore purchase completes. */ internal fun restorePurchases(completionCallback: CBCallback.RestorePurchaseCallback) { - this.completionCallback = completionCallback + this.restorePurchaseCallBack = completionCallback onConnected({ status -> queryPurchaseHistoryFromStore(status) }, { error -> @@ -314,11 +311,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { storeTransactions.addAll(purchaseHistoryList) CBRestorePurchaseManager.fetchStoreSubscriptionStatus( storeTransactions, - completionCallback + restorePurchaseCallBack ) } } else { - completionCallback.onError( + restorePurchaseCallBack.onError( connectionError ) } @@ -363,7 +360,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } purchaseTransactionList(purchaseHistoryList) } else { - completionCallback.onError(throwCBException(billingResult)) + restorePurchaseCallBack.onError(throwCBException(billingResult)) } } } From fd2abeb42246e1e8f54ffce48a2a86c49b9f7610 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Fri, 12 May 2023 11:36:09 +0530 Subject: [PATCH 12/84] Version bump and updated README.md --- README.md | 4 ++-- chargebee/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9aee583..da8316b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.16' +implementation 'com.chargebee:chargebee-android:1.0.17' ``` ## Example project @@ -403,7 +403,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.16' + implementation 'com.chargebee:chargebee-android:1.0.17' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 1e520f8..564fb9c 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.16" + versionName "1.0.17" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From 306dfb3efd52f034697ffa8c4de0d1beb795614e Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 30 May 2023 12:02:45 +0530 Subject: [PATCH 13/84] Imp - cache receipt and retry mechanism And added new validateReceiptWithChargebee method in BillingClientManager --- .../example/billing/BillingActivity.java | 2 +- .../example/billing/BillingViewModel.kt | 26 +++++++++++++++++-- .../java/com/chargebee/android/CBResult.kt | 4 +-- .../billingservice/BillingClientManager.kt | 17 ++++++++++++ .../android/billingservice/CBPurchase.kt | 23 ++++++++++++++-- 5 files changed, 65 insertions(+), 7 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 7ef409c..6f17df9 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -149,7 +149,7 @@ private void purchaseProduct(String customerId){ this.billingViewModel.purchaseProduct(productList.get(position), customerId); } private void purchaseProduct(){ - this.billingViewModel.purchaseProduct(productList.get(position), cbCustomer); + this.billingViewModel.purchaseProduct(this,productList.get(position), cbCustomer); } 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 39164fc..321815f 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -1,5 +1,6 @@ package com.chargebee.example.billing +import android.content.Context import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -27,7 +28,7 @@ class BillingViewModel : ViewModel() { var entitlementsResult: MutableLiveData = MutableLiveData() private var subscriptionId: String = "" - fun purchaseProduct(product: CBProduct, customer: CBCustomer) { + fun purchaseProduct(context: Context,product: CBProduct, customer: CBCustomer) { CBPurchase.purchaseProduct(product, customer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { @@ -37,7 +38,11 @@ class BillingViewModel : ViewModel() { } override fun onError(error: CBException) { try { - cbException.postValue(error) + if (error.httpStatusCode!! in 500..599) { + validateReceipt(context = context, product = product) + }else { + cbException.postValue(error) + } }catch (exp: Exception){ Log.i(TAG, "Exception :${exp.message}") } @@ -62,6 +67,23 @@ class BillingViewModel : ViewModel() { }) } + fun validateReceipt(context: Context, product: CBProduct) { + CBPurchase.validateReceipt(context = context, product = product, completionCallback = object : CBCallback.PurchaseCallback{ + override fun onSuccess(result: ReceiptDetail, status:Boolean) { + Log.i(TAG, "Subscription ID: ${result.subscription_id}") + Log.i(TAG, "Plan ID: ${result.plan_id}") + productPurchaseResult.postValue(status) + } + override fun onError(error: CBException) { + try { + cbException.postValue(error) + }catch (exp: Exception){ + Log.i(TAG, "Exception :${exp.message}") + } + } + }) + } + fun retrieveProductIdentifers(queryParam: Array){ CBPurchase.retrieveProductIdentifers(queryParam) { when (it) { diff --git a/chargebee/src/main/java/com/chargebee/android/CBResult.kt b/chargebee/src/main/java/com/chargebee/android/CBResult.kt index 6299dc8..1e8e8df 100644 --- a/chargebee/src/main/java/com/chargebee/android/CBResult.kt +++ b/chargebee/src/main/java/com/chargebee/android/CBResult.kt @@ -74,14 +74,14 @@ internal fun responseFromServer(response: Response): ChargebeeResult ) ) } - 401 -> { + in 400..499 -> { return ChargebeeResult.Error( exp = CBException( error = ErrorDetail(response.errorBody()?.string(), httpStatusCode = response.code()) ) ) } - 400, 500 -> { + in 500..599 -> { return ChargebeeResult.Error( exp = CBException( error = ErrorDetail(response.errorBody()?.string(), httpStatusCode = response.code()) 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 c774fc3..0168612 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -417,4 +417,21 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } } + + internal fun validateReceiptWithChargebee(product: CBProduct, completionCallback: CBCallback.PurchaseCallback ) { + onConnected({ status -> + if (status) + queryPurchaseHistory { purchaseHistoryList -> + val purchaseTransaction = purchaseHistoryList.filter { + it.productId.first() == product.productId + } + validateReceipt(purchaseTransaction.first().purchaseToken, product) + } else + completionCallback.onError( + connectionError + ) + }, { error -> + completionCallback.onError(error) + }) + } } 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 3e756ff..0f386e4 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -3,7 +3,6 @@ package com.chargebee.android.billingservice import android.content.Context import android.text.TextUtils import android.util.Log -import com.android.billingclient.api.BillingClient import com.chargebee.android.Chargebee import com.chargebee.android.ErrorDetail import com.chargebee.android.exceptions.* @@ -141,9 +140,29 @@ object CBPurchase { sharedInstance(context).restorePurchases(completionCallback) } - /* Chargebee Method - used to validate the receipt of purchase */ + /** + * This method will be used to validate the receipt with Chargebee. + * In case of failed syncing with Chargebee during the purchase flow + * + * @param [context] Current activity context + * @param [productId] Product Identifier. + * @param [customer] Optional. Customer Object. + * @param [completionCallback] The listener will be called when validate receipt completes. + */ @JvmStatic fun validateReceipt( + context: Context, + product: CBProduct, + customer: CBCustomer? = null, + completionCallback: CBCallback.PurchaseCallback + ) { + this.customer = customer + sharedInstance(context).validateReceiptWithChargebee(product, completionCallback) + } + + /* Chargebee Method - used to validate the receipt of purchase */ + @JvmStatic + internal fun validateReceipt( purchaseToken: String, product: CBProduct, completion: (ChargebeeResult) -> Unit From 16aa7ae41553e76d84ae217358700c9239cd72e9 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 30 May 2023 13:43:12 +0530 Subject: [PATCH 14/84] CBCustomer object added and unit test --- .../example/billing/BillingViewModel.kt | 43 +++++--- .../BillingClientManagerTest.kt | 101 ++++++++++++++++-- 2 files changed, 118 insertions(+), 26 deletions(-) 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 321815f..1475245 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -40,10 +40,10 @@ class BillingViewModel : ViewModel() { try { if (error.httpStatusCode!! in 500..599) { validateReceipt(context = context, product = product) - }else { + } else { cbException.postValue(error) } - }catch (exp: Exception){ + } catch (exp: Exception) { Log.i(TAG, "Exception :${exp.message}") } } @@ -67,21 +67,32 @@ class BillingViewModel : ViewModel() { }) } - fun validateReceipt(context: Context, product: CBProduct) { - CBPurchase.validateReceipt(context = context, product = product, completionCallback = object : CBCallback.PurchaseCallback{ - override fun onSuccess(result: ReceiptDetail, status:Boolean) { - Log.i(TAG, "Subscription ID: ${result.subscription_id}") - Log.i(TAG, "Plan ID: ${result.plan_id}") - productPurchaseResult.postValue(status) - } - override fun onError(error: CBException) { - try { - cbException.postValue(error) - }catch (exp: Exception){ - Log.i(TAG, "Exception :${exp.message}") + 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, + customer = customer, + completionCallback = object : CBCallback.PurchaseCallback { + override fun onSuccess(result: ReceiptDetail, status: Boolean) { + Log.i(TAG, "Subscription ID: ${result.subscription_id}") + Log.i(TAG, "Plan ID: ${result.plan_id}") + productPurchaseResult.postValue(status) } - } - }) + + override fun onError(error: CBException) { + try { + cbException.postValue(error) + } catch (exp: Exception) { + Log.i(TAG, "Exception :${exp.message}") + } + } + }) } fun retrieveProductIdentifers(queryParam: Array){ 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 427191d..17ba9fe 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -1,4 +1,4 @@ -package com.chargebee.android.resources +package com.chargebee.android.billingservice import android.content.Context import android.os.Build @@ -6,15 +6,14 @@ import androidx.test.core.app.ApplicationProvider import com.android.billingclient.api.* import com.chargebee.android.Chargebee import com.chargebee.android.ErrorDetail -import com.chargebee.android.billingservice.BillingClientManager -import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBCallback.ListProductsCallback -import com.chargebee.android.billingservice.CBPurchase import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.CBProductIDResult import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.CBProduct import com.chargebee.android.network.* +import com.chargebee.android.resources.CatalogVersion +import com.chargebee.android.resources.ReceiptResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -36,9 +35,10 @@ import kotlin.collections.ArrayList @RunWith(MockitoJUnitRunner::class) @Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) -class BillingClientManagerTest { +class BillingClientManagerTest { private var billingClientManager: BillingClientManager? = null + @Mock lateinit var billingClient: BillingClient @@ -46,12 +46,20 @@ class BillingClientManagerTest { lateinit var skuDetails: SkuDetails private var mContext: Context? = null - private var callBack : ListProductsCallback>? = null - private var callBackPurchase : CBCallback.PurchaseCallback? = null + private var callBack: ListProductsCallback>? = null + private var callBackPurchase: CBCallback.PurchaseCallback? = null private val productIdList = arrayListOf("merchant.pro.android", "merchant.premium.android") - private var customer = CBCustomer("test","android","test","test@gmail.com") + private var customer = CBCustomer("test", "android", "test", "test@gmail.com") private var customerId: String = "test" - + private val purchaseToken = "56sadmnagdjsd" + private val product = CBProduct( + "merchant.premium.test.android", + "Premium Plan (Chargebee Example)", + "₹2,650.00", + skuDetails, + true + ) + private val receiptDetail = ReceiptDetail("subscriptionId", "customerId", "planId") @Before fun setUp() { @@ -318,7 +326,6 @@ class BillingClientManagerTest { customer, Chargebee.channel ) - val receiptDetail = ReceiptDetail("subscriptionId","customerId","planId") val response = CBReceiptResponse(receiptDetail) CoroutineScope(Dispatchers.IO).launch { @@ -445,4 +452,78 @@ class BillingClientManagerTest { }) } } + + @Test + fun test_validateReceiptWithChargebee_success() { + val params = Params( + purchaseToken, + product.productId, + customer, + Chargebee.channel + ) + val response = CBReceiptResponse(receiptDetail) + CoroutineScope(Dispatchers.IO).launch { + mContext?.let { + CBPurchase.validateReceipt( + it, + product, + customer, + completionCallback = object : CBCallback.PurchaseCallback { + override fun onSuccess(result: ReceiptDetail, status: Boolean) { + assertThat(result, instanceOf(ReceiptDetail::class.java)) + } + + override fun onError(error: CBException) { + println(" Error : ${error.message}") + } + }) + } + } + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( + ChargebeeResult.Success( + response + ) + ) + verify(ReceiptResource(), times(1)).validateReceipt(params) + verify(CBReceiptRequestBody("receipt", "", null, ""), times(1)).toCBReceiptReqBody() + } + } + + @Test + fun test_validateReceiptWithChargebee_error() { + val params = Params( + purchaseToken, + product.productId, + customer, + Chargebee.channel + ) + val exception = CBException(ErrorDetail("Error")) + CoroutineScope(Dispatchers.IO).launch { + mContext?.let { + CBPurchase.validateReceipt( + it, + product, + customer, + completionCallback = object : CBCallback.PurchaseCallback { + override fun onSuccess(result: ReceiptDetail, status: Boolean) { + assertThat(result, instanceOf(ReceiptDetail::class.java)) + } + + override fun onError(error: CBException) { + println(" Error : ${error.message}") + } + }) + } + } + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( + ChargebeeResult.Error( + exception + ) + ) + verify(ReceiptResource(), times(1)).validateReceipt(params) + verify(CBReceiptRequestBody("receipt", "", null, ""), times(1)).toCBReceiptReqBody() + } + } } \ No newline at end of file From 1de683e61e6ebcbbcf18d3c89eac5cb7a552c5bf Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 30 May 2023 23:31:08 +0530 Subject: [PATCH 15/84] Updated test case --- .../BillingClientManagerTest.kt | 134 +++++++----------- 1 file changed, 52 insertions(+), 82 deletions(-) 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 17ba9fe..b017906 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -42,9 +42,6 @@ class BillingClientManagerTest { @Mock lateinit var billingClient: BillingClient - @Mock - lateinit var skuDetails: SkuDetails - private var mContext: Context? = null private var callBack: ListProductsCallback>? = null private var callBackPurchase: CBCallback.PurchaseCallback? = null @@ -52,12 +49,11 @@ class BillingClientManagerTest { private var customer = CBCustomer("test", "android", "test", "test@gmail.com") private var customerId: String = "test" private val purchaseToken = "56sadmnagdjsd" - private val product = CBProduct( - "merchant.premium.test.android", - "Premium Plan (Chargebee Example)", - "₹2,650.00", - skuDetails, - true + private val params = Params( + "purchaseToken", + "product.productId", + customer, + Chargebee.channel ) private val receiptDetail = ReceiptDetail("subscriptionId", "customerId", "planId") @@ -248,8 +244,9 @@ class BillingClientManagerTest { } @Test fun test_purchaseProduct_success(){ - // 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 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 { @@ -281,6 +278,7 @@ class BillingClientManagerTest { 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( @@ -300,12 +298,9 @@ class BillingClientManagerTest { @Test fun test_validateReceipt_success(){ val purchaseToken = "56sadmnagdjsd" - 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 products = CBProduct("merchant.premium.android","Premium Plan (Chargebee Example)","₹2,650.00", skuDetails,true) val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { - CBPurchase.validateReceipt(purchaseToken, products) { + CBPurchase.validateReceipt(purchaseToken, "productId") { when (it) { is ChargebeeResult.Success -> { lock.countDown() @@ -319,15 +314,7 @@ class BillingClientManagerTest { } } lock.await() - - val params = Params( - purchaseToken, - products.productId, - customer, - Chargebee.channel - ) val response = CBReceiptResponse(receiptDetail) - CoroutineScope(Dispatchers.IO).launch { Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( ChargebeeResult.Success( @@ -341,12 +328,8 @@ class BillingClientManagerTest { @Test fun test_validateReceipt_error(){ val purchaseToken = "56sadmnagdjsd" - val jsonDetails = "{\"productId\":\"merchant.premium.test.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? = null - val products = CBProduct("merchant.premium.test.android","Premium Plan (Chargebee Example)","₹2,650.00", skuDetails,true) CoroutineScope(Dispatchers.IO).launch { - CBPurchase.validateReceipt(purchaseToken, products) { + CBPurchase.validateReceipt(purchaseToken, "products") { when (it) { is ChargebeeResult.Success -> { assertThat(it, instanceOf(CBReceiptResponse::class.java)) @@ -357,13 +340,6 @@ class BillingClientManagerTest { } } } - - val params = Params( - purchaseToken, - products.productId, - customer, - Chargebee.channel - ) val exception = CBException(ErrorDetail("Error")) CoroutineScope(Dispatchers.IO).launch { Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( @@ -379,6 +355,9 @@ class BillingClientManagerTest { @Test fun test_purchaseProductWithEmptyCBCustomer_success(){ val customer = CBCustomer("","","","") + 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 { @@ -408,6 +387,9 @@ class BillingClientManagerTest { @Test fun test_purchaseProductWithCBCustomer_success(){ + 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 { @@ -436,6 +418,9 @@ class BillingClientManagerTest { } @Test fun test_purchaseProductWithCBCustomer_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( @@ -455,65 +440,50 @@ class BillingClientManagerTest { @Test fun test_validateReceiptWithChargebee_success() { - val params = Params( - purchaseToken, - product.productId, - customer, - Chargebee.channel - ) val response = CBReceiptResponse(receiptDetail) CoroutineScope(Dispatchers.IO).launch { - mContext?.let { - CBPurchase.validateReceipt( - it, - product, - customer, - completionCallback = object : CBCallback.PurchaseCallback { - override fun onSuccess(result: ReceiptDetail, status: Boolean) { - assertThat(result, instanceOf(ReceiptDetail::class.java)) - } - - override fun onError(error: CBException) { - println(" Error : ${error.message}") - } - }) + CBPurchase.validateReceipt( + purchaseToken = "purchaseToken", + productId = "productId" + ) { + when (it) { + is ChargebeeResult.Success -> { + assertThat(it, instanceOf(ReceiptDetail::class.java)) + } + is ChargebeeResult.Error -> { + println(" Error : ${it.exp}") + } + } } } - CoroutineScope(Dispatchers.IO).launch { - Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( - ChargebeeResult.Success( - response + CoroutineScope(Dispatchers.IO).launch { + Mockito.`when`(params.let { ReceiptResource().validateReceipt(it) }).thenReturn( + ChargebeeResult.Success( + response + ) ) - ) - verify(ReceiptResource(), times(1)).validateReceipt(params) - verify(CBReceiptRequestBody("receipt", "", null, ""), times(1)).toCBReceiptReqBody() + verify(ReceiptResource(), times(1)).validateReceipt(params) + verify(CBReceiptRequestBody("receipt", "", null, ""), times(1)).toCBReceiptReqBody() + + } } - } @Test fun test_validateReceiptWithChargebee_error() { - val params = Params( - purchaseToken, - product.productId, - customer, - Chargebee.channel - ) val exception = CBException(ErrorDetail("Error")) CoroutineScope(Dispatchers.IO).launch { - mContext?.let { - CBPurchase.validateReceipt( - it, - product, - customer, - completionCallback = object : CBCallback.PurchaseCallback { - override fun onSuccess(result: ReceiptDetail, status: Boolean) { - assertThat(result, instanceOf(ReceiptDetail::class.java)) - } - - override fun onError(error: CBException) { - println(" Error : ${error.message}") - } - }) + CBPurchase.validateReceipt( + purchaseToken = "purchaseToken", + productId = "productId" + ) { + when (it) { + is ChargebeeResult.Success -> { + assertThat(it, instanceOf(ReceiptDetail::class.java)) + } + is ChargebeeResult.Error -> { + println(" Error : ${it.exp}") + } + } } } CoroutineScope(Dispatchers.IO).launch { From cb724ac6d44c1ddc3dcf6b4d3942d1b8094439c0 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 31 May 2023 13:55:03 +0530 Subject: [PATCH 16/84] Update example app to handle server not responding use case and README.md --- README.md | 22 +++++++++++++++++++ .../example/billing/BillingActivity.java | 2 +- .../example/billing/BillingViewModel.kt | 8 +++++-- .../billingservice/BillingClientManager.kt | 1 + 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da8316b..b72b4e5 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,28 @@ These are the possible error codes and their descriptions: | `BillingErrorCode.SERVICE_DISCONNECTED` | The app is not connected to the Play Store service via the Google Play Billing Library. | | `BillingErrorCode.UNKNOWN` | Unknown error occurred. | +##### Synchronization of Google Play Store Purchases with Chargebee through Receipt Validation +Receipt validation is crucial to ensure that the purchases made by your users are synced with Chargebee. In rare cases, when a purchase is made at the Google Play Store, and the network connection goes off or the server not responding, the purchase details may not be updated in Chargebee. In such cases, you can use a retry mechanism by following these steps: + +* 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(). + +Use the function available for the retry mechanism. +##### Function for subscriptions + +```kotlin +CBPurchase.validateReceipt(context = current activity context, product = CBProduct, customer = CBCustomer, object : CBCallback.PurchaseCallback { + override fun onSuccess(result: ReceiptDetail, status: Boolean) { + Log.i(TAG, "$status") + Log.i(TAG, "${result.subscription_id}") + Log.i(TAG, "${result.plan_id}") + } + 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/billing/BillingActivity.java b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java index 6f17df9..a8fda6d 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -146,7 +146,7 @@ public void onClick(View v) { } private void purchaseProduct(String customerId){ - this.billingViewModel.purchaseProduct(productList.get(position), customerId); + this.billingViewModel.purchaseProduct(this, productList.get(position), customerId); } private void purchaseProduct(){ this.billingViewModel.purchaseProduct(this,productList.get(position), cbCustomer); 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 1475245..f59df85 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -49,7 +49,7 @@ class BillingViewModel : ViewModel() { } }) } - fun purchaseProduct(product: CBProduct, customerId: String) { + fun purchaseProduct(context: Context, product: CBProduct, customerId: String) { CBPurchase.purchaseProduct(product, customerId, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { @@ -59,7 +59,11 @@ class BillingViewModel : ViewModel() { } override fun onError(error: CBException) { try { - cbException.postValue(error) + if (error.httpStatusCode!! in 500..599) { + validateReceipt(context = context, product = product) + } else { + cbException.postValue(error) + } }catch (exp: Exception){ Log.i(TAG, "Exception :${exp.message}") } 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 0168612..777600f 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -419,6 +419,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } internal fun validateReceiptWithChargebee(product: CBProduct, completionCallback: CBCallback.PurchaseCallback ) { + this.purchaseCallBack = completionCallback onConnected({ status -> if (status) queryPurchaseHistory { purchaseHistoryList -> From 6ccf8354a3fa2df02be9cca7ecf8a2f6947d407d Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 31 May 2023 21:45:14 +0530 Subject: [PATCH 17/84] Handled offline use case cache the product and retry validateReceipt --- README.md | 1 + .../chargebee/example/ExampleApplication.kt | 76 +++++++++++++++++-- .../example/billing/BillingViewModel.kt | 21 +++-- .../com/chargebee/example/util/NetworkUtil.kt | 47 ++++++++++++ .../chargebee/android/models/ResultHandler.kt | 10 ++- 5 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/chargebee/example/util/NetworkUtil.kt diff --git a/README.md b/README.md index b72b4e5..0e5ecec 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ These are the possible error codes and their descriptions: ##### Synchronization of Google Play Store Purchases with Chargebee through Receipt Validation Receipt validation is crucial to ensure that the purchases made by your users are synced with Chargebee. In rare cases, when a purchase is made at the Google Play Store, and the network connection goes off or the server not responding, the purchase details may not be updated in Chargebee. In such cases, you can use a retry mechanism by following these steps: +* 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(). diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index e6b554a..415c10f 100644 --- a/app/src/main/java/com/chargebee/example/ExampleApplication.kt +++ b/app/src/main/java/com/chargebee/example/ExampleApplication.kt @@ -2,22 +2,84 @@ package com.chargebee.example import android.app.Application import android.content.Context -import android.net.ConnectivityManager import com.chargebee.android.Chargebee +import android.content.SharedPreferences +import android.util.Log +import com.chargebee.android.billingservice.CBCallback +import com.chargebee.android.billingservice.CBPurchase +import com.chargebee.android.exceptions.CBException +import com.chargebee.android.models.CBProduct +import com.chargebee.android.network.CBCustomer +import com.chargebee.android.network.ReceiptDetail +import com.chargebee.example.util.NetworkUtil -class ExampleApplication: Application() { +class ExampleApplication : Application(), NetworkUtil.NetworkListener { + private lateinit var networkUtil: NetworkUtil + private lateinit var sharedPreference: SharedPreferences + lateinit var mContext: Context override fun onCreate() { super.onCreate() + mContext = this + networkUtil = NetworkUtil(mContext, this) + networkUtil.registerCallbackEvents() + sharedPreference = mContext.getSharedPreferences("PREFERENCE_NAME", Context.MODE_PRIVATE) + } - if (isInternetAvailable(this)) - // Please add site/app details as required + override fun onNetworkConnectionAvailable() { Chargebee.configure(site = "", publishableApiKey= "",sdkKey= "", packageName = this.packageName) + val productId = sharedPreference.getString("productId", "") + if (productId?.isNotEmpty() == true) { + val productList = ArrayList() + productList.add(productId) + retrieveProducts(productList) + } + } + + override fun onNetworkConnectionLost() { + Log.e(javaClass.simpleName, "Network connectivity not available") + } + + private fun retrieveProducts(productIdList: ArrayList) { + CBPurchase.retrieveProducts( + this, + productIdList, + object : CBCallback.ListProductsCallback> { + override fun onSuccess(productIDs: ArrayList) { + validateReceipt(mContext, productIDs.first()) + } + override fun onError(error: CBException) { + Log.e(javaClass.simpleName, "Exception: $error") + } + }) } - private fun isInternetAvailable(context: Context): Boolean { - val conMgr = context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager - return conMgr.activeNetworkInfo != null + 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, + customer = customer, + completionCallback = object : CBCallback.PurchaseCallback { + override fun onSuccess(result: ReceiptDetail, status: Boolean) { + // Clear the local cache once receipt validation success + val editor = sharedPreference.edit() + editor.clear().apply() + Log.i(javaClass.simpleName, "Subscription ID: ${result.subscription_id}") + Log.i(javaClass.simpleName, "Plan ID: ${result.plan_id}") + Log.i(javaClass.simpleName, "Customer ID: ${result.customer_id}") + 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/billing/BillingViewModel.kt b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt index f59df85..02bba27 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -1,6 +1,7 @@ package com.chargebee.example.billing import android.content.Context +import android.content.SharedPreferences import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -14,7 +15,6 @@ import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.* import com.chargebee.android.network.CBCustomer import com.chargebee.android.network.ReceiptDetail -import com.google.gson.Gson class BillingViewModel : ViewModel() { @@ -27,9 +27,10 @@ class BillingViewModel : ViewModel() { var error: MutableLiveData = MutableLiveData() var entitlementsResult: MutableLiveData = MutableLiveData() private var subscriptionId: String = "" + private lateinit var sharedPreference : SharedPreferences fun purchaseProduct(context: Context,product: CBProduct, customer: CBCustomer) { - + sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) CBPurchase.purchaseProduct(product, customer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { Log.i(TAG, "Subscription ID: ${result.subscription_id}") @@ -40,6 +41,9 @@ class BillingViewModel : ViewModel() { try { if (error.httpStatusCode!! in 500..599) { validateReceipt(context = context, product = product) + } else if (error.httpStatusCode!! == 120){ + storeInLocal(product.productId) + cbException.postValue(error) } else { cbException.postValue(error) } @@ -50,7 +54,7 @@ class BillingViewModel : ViewModel() { }) } fun purchaseProduct(context: Context, product: CBProduct, customerId: String) { - + sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) CBPurchase.purchaseProduct(product, customerId, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { Log.i(TAG, "Subscription ID: ${result.subscription_id}") @@ -61,10 +65,13 @@ class BillingViewModel : ViewModel() { try { if (error.httpStatusCode!! in 500..599) { validateReceipt(context = context, product = product) + } else if (error.httpStatusCode!! == 120) { + storeInLocal(product.productId) + cbException.postValue(error) } else { cbException.postValue(error) } - }catch (exp: Exception){ + } catch (exp: Exception) { Log.i(TAG, "Exception :${exp.message}") } } @@ -168,5 +175,9 @@ class BillingViewModel : ViewModel() { } } } - + private fun storeInLocal(productId: String){ + val editor = sharedPreference.edit() + editor.putString("productId", productId) + editor.apply() + } } \ No newline at end of file diff --git a/app/src/main/java/com/chargebee/example/util/NetworkUtil.kt b/app/src/main/java/com/chargebee/example/util/NetworkUtil.kt new file mode 100644 index 0000000..911fa3e --- /dev/null +++ b/app/src/main/java/com/chargebee/example/util/NetworkUtil.kt @@ -0,0 +1,47 @@ +package com.chargebee.example.util + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.util.Log + +class NetworkUtil(context: Context, callback: NetworkListener) : + ConnectivityManager.NetworkCallback() { + private var mNetworkRequest: NetworkRequest? = null + private var mConnectivityManager: ConnectivityManager? = null + private var callback: NetworkListener + + interface NetworkListener { + fun onNetworkConnectionAvailable() + fun onNetworkConnectionLost() + } + + init { + mNetworkRequest = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() + mConnectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + this.callback = callback + } + + override fun onAvailable(network: Network) { + super.onAvailable(network) + Log.i(javaClass.simpleName, "Connected to network"); + callback.onNetworkConnectionAvailable() + + } + + override fun onLost(network: Network) { + super.onLost(network) + Log.i(javaClass.simpleName, "Lost network connection"); + callback.onNetworkConnectionLost() + } + + fun registerCallbackEvents() { + Log.i(javaClass.simpleName, "Register callback events to trigger network connectivity") + mConnectivityManager!!.registerNetworkCallback(mNetworkRequest!!, this) + } +} \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt b/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt index 09f23a9..293d00d 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt @@ -48,10 +48,14 @@ internal class ResultHandler { ChargebeeResult.Error(ex) } catch (ex: UnknownHostException) { print("failed: ${ex.message}") - ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message))) + ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 120))) } catch (ex: Exception) { - logger?.error(ex.message ?: "failed") - ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message))) + try { + logger?.error(ex.message ?: "failed") + } catch (ex: Exception) { + print("Exception : ${ex.message}") + } + ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 120))) } completion(result) } From a2d5a9a6724311cb7a102f5e0c25fea55a4c1e3d Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 6 Jun 2023 17:44:39 +0530 Subject: [PATCH 18/84] Addressed PR comments --- README.md | 2 +- .../chargebee/example/billing/BillingViewModel.kt | 13 +++++++------ .../chargebee/android/billingservice/CBPurchase.kt | 4 ++-- .../com/chargebee/android/models/ResultHandler.kt | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0e5ecec..fbed89f 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ Receipt validation is crucial to ensure that the purchases made by your users ar * 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(). Use the function available for the retry mechanism. -##### Function for subscriptions +##### Function for validating the receipt ```kotlin CBPurchase.validateReceipt(context = current activity context, product = CBProduct, customer = CBCustomer, object : CBCallback.PurchaseCallback { 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 02bba27..932d568 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -30,6 +30,7 @@ class BillingViewModel : ViewModel() { private lateinit var sharedPreference : SharedPreferences 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. sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) CBPurchase.purchaseProduct(product, customer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { @@ -39,11 +40,10 @@ class BillingViewModel : ViewModel() { } override fun onError(error: CBException) { try { + // Handled server not responding and offline if (error.httpStatusCode!! in 500..599) { - validateReceipt(context = context, product = product) - } else if (error.httpStatusCode!! == 120){ storeInLocal(product.productId) - cbException.postValue(error) + validateReceipt(context = context, product = product) } else { cbException.postValue(error) } @@ -64,10 +64,8 @@ class BillingViewModel : ViewModel() { override fun onError(error: CBException) { try { if (error.httpStatusCode!! in 500..599) { - validateReceipt(context = context, product = product) - } else if (error.httpStatusCode!! == 120) { storeInLocal(product.productId) - cbException.postValue(error) + validateReceipt(context = context, product = product) } else { cbException.postValue(error) } @@ -93,6 +91,9 @@ class BillingViewModel : ViewModel() { override fun onSuccess(result: ReceiptDetail, status: Boolean) { Log.i(TAG, "Subscription ID: ${result.subscription_id}") Log.i(TAG, "Plan ID: ${result.plan_id}") + // Clear the local cache once receipt validation success + val editor = sharedPreference.edit() + editor.clear().apply() productPurchaseResult.postValue(status) } 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 0f386e4..595a477 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -141,8 +141,8 @@ object CBPurchase { } /** - * This method will be used to validate the receipt with Chargebee. - * In case of failed syncing with Chargebee during the purchase flow + * 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. diff --git a/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt b/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt index 293d00d..ba01177 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt @@ -48,14 +48,14 @@ internal class ResultHandler { ChargebeeResult.Error(ex) } catch (ex: UnknownHostException) { print("failed: ${ex.message}") - ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 120))) + ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 502))) } catch (ex: Exception) { try { logger?.error(ex.message ?: "failed") } catch (ex: Exception) { print("Exception : ${ex.message}") } - ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 120))) + ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 502))) } completion(result) } From ebd4b7f79380e320a03b491291a1cd3f40686532 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 6 Jun 2023 20:22:33 +0530 Subject: [PATCH 19/84] Version bump and updated README.md --- README.md | 4 ++-- chargebee/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fbed89f..86c0823 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.17' +implementation 'com.chargebee:chargebee-android:1.0.18' ``` ## Example project @@ -426,7 +426,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.17' + implementation 'com.chargebee:chargebee-android:1.0.18' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 564fb9c..3e8ccea 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.17" + versionName "1.0.18" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From c3308a911ca10d0a23ea5295fdf57a455ff80b74 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 3 Jul 2023 13:12:25 +0530 Subject: [PATCH 20/84] 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 21/84] 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 22/84] 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 23/84] 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 24/84] 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 25/84] 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 26/84] 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 27/84] 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 703db22b14fec2defa5c55e9b6f75dee58ea4dce Mon Sep 17 00:00:00 2001 From: cb-sabuj Date: Fri, 7 Jul 2023 18:35:45 +0530 Subject: [PATCH 28/84] Added Plan ID related information --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 86c0823..729a5d5 100644 --- a/README.md +++ b/README.md @@ -551,6 +551,8 @@ The above function will determine your product catalog version in Chargebee and 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. + This function also returns the plan ID associated with a subscription. You can associate JSON metadata with the Apple App Store plans in Chargebee and retrieve the same by passing plan ID to the SDK function - [retrievePlan](https://github.com/chargebee/chargebee-ios#get-plan-details)(PC 1.0) or [retrieveItem](https://github.com/chargebee/chargebee-ios#get-item-details)(PC 2.0). + ##### Returns Plan Object This function returns the plan ID associated with a subscription. You can associate JSON metadata with the Google Play Store plans in Chargebee and retrieve the same by passing plan ID to the SDK method - [retrievePlan](https://github.com/chargebee/chargebee-android#get-plan-details)(PC 1.0) or [retrieveItem](https://github.com/chargebee/chargebee-android#get-item-details)(PC 2.0). From 32a22248da53d7a496defd494a6c857a76b7e0cf Mon Sep 17 00:00:00 2001 From: cb-sabuj Date: Fri, 7 Jul 2023 18:44:11 +0530 Subject: [PATCH 29/84] fix --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 729a5d5..8e6e0be 100644 --- a/README.md +++ b/README.md @@ -551,11 +551,7 @@ The above function will determine your product catalog version in Chargebee and 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. - This function also returns the plan ID associated with a subscription. You can associate JSON metadata with the Apple App Store plans in Chargebee and retrieve the same by passing plan ID to the SDK function - [retrievePlan](https://github.com/chargebee/chargebee-ios#get-plan-details)(PC 1.0) or [retrieveItem](https://github.com/chargebee/chargebee-ios#get-item-details)(PC 2.0). - - ##### Returns Plan Object - - This function returns the plan ID associated with a subscription. You can associate JSON metadata with the Google Play Store plans in Chargebee and retrieve the same by passing plan ID to the SDK method - [retrievePlan](https://github.com/chargebee/chargebee-android#get-plan-details)(PC 1.0) or [retrieveItem](https://github.com/chargebee/chargebee-android#get-item-details)(PC 2.0). + This function also returns the plan ID associated with a subscription. You can associate JSON metadata with the Google Play Store plans in Chargebee and retrieve the same by passing plan ID to the SDK method - [retrievePlan](https://github.com/chargebee/chargebee-android#get-plan-details)(PC 1.0) or [retrieveItem](https://github.com/chargebee/chargebee-android#get-item-details)(PC 2.0). #### Get Subscription Status for Existing Subscribers From 9d163426569c6487175dcf3868b35d0b76739722 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 10 Jul 2023 18:52:30 +0530 Subject: [PATCH 30/84] 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 From 268d22cccd37d492c3ffcd55ce24a5d3b8b80e34 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 11 Jul 2023 10:41:19 +0530 Subject: [PATCH 31/84] Version bump and updated README.md --- README.md | 4 ++-- chargebee/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e116c00..0f847a9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.18' +implementation 'com.chargebee:chargebee-android:1.0.19' ``` ## Example project @@ -467,7 +467,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.18' + implementation 'com.chargebee:chargebee-android:1.0.19' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 3e8ccea..fadcaad 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.18" + versionName "1.0.19" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From cf5b5841b08c79646d7f9428955b20223eeb5b87 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Tue, 11 Jul 2023 17:27:59 +0530 Subject: [PATCH 32/84] fix: adds additional logs prior to purchase --- .../android/billingservice/CBPurchase.kt | 26 +++++++++++++-- .../com/chargebee/android/loggers/CBLogger.kt | 3 +- .../chargebee/android/models/ResultHandler.kt | 32 ++++++++++++++++--- .../android/resources/LoggerResource.kt | 28 ++++++++++------ 4 files changed, 72 insertions(+), 17 deletions(-) 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 49bb331..585efb4 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -87,6 +87,7 @@ object CBPurchase { private fun purchaseProduct(product: CBProduct, callback: CBCallback.PurchaseCallback) { isSDKKeyValid({ + log(customer, product) billingClientManager?.purchase(product, callback) }, { callback.onError(it) @@ -108,7 +109,9 @@ object CBPurchase { ) { this.customer = customer this.productType = productType + isSDKKeyValid({ + log(CBPurchase.customer, product, productType) billingClientManager?.purchaseNonSubscriptionProduct(product, callback) }, { callback.onError(it) @@ -207,7 +210,8 @@ object CBPurchase { productId: String, completion: (ChargebeeResult) -> Unit ) { - val logger = CBLogger(name = "buy", action = "process_purchase_command") + val logger = CBLogger(name = "buy", action = "process_purchase_command", + additionalInfo = mapOf("customerId" to (customer?.id ?: ""), "product" to productId, "purchaseToken" to purchaseToken)) val params = Params( purchaseToken, productId, @@ -258,7 +262,8 @@ object CBPurchase { productId: String, completion: (ChargebeeResult) -> Unit ) { - val logger = CBLogger(name = "buy", action = "one_time_purchase") + val logger = CBLogger(name = "buy", action = "one_time_purchase", + additionalInfo = mapOf("customerId" to (customer?.id ?: ""), "product" to productId, "purchaseToken" to purchaseToken)) val params = Params( purchaseToken, productId, @@ -374,4 +379,21 @@ object CBPurchase { } return billingClientManager as BillingClientManager } + + private fun log(customer: CBCustomer?, product: CBProduct, productType: OneTimeProductType? = null) { + val additionalInfo = additionalInfo(customer, product, productType) + val logger = CBLogger( + name = "buy", + action = "before_purchase_command", + additionalInfo = additionalInfo + ) + ResultHandler.safeExecute { logger.info() } + } + private fun additionalInfo(customer: CBCustomer?, product: CBProduct, productType: OneTimeProductType? = null): Map { + val map = mutableMapOf("product" to product.productId) + customer?.let { "customerId" to (it.id ?: "") } + productType?.let { map["productType"] = it.toString() } + return map + } + } \ No newline at end of file diff --git a/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt b/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt index b492017..4a9aebd 100644 --- a/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt +++ b/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt @@ -6,7 +6,8 @@ import com.chargebee.android.resources.LogType import com.chargebee.android.resources.LoggerResource class CBLogger(private val name: String, - private val action: String) { + private val action: String, + private val additionalInfo: Map? = null) { suspend fun error(message: String, code: Int? = null) { postLog(LogType.ERROR, message, code) diff --git a/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt b/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt index ba01177..b45b142 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/ResultHandler.kt @@ -33,6 +33,7 @@ internal class ResultHandler { completion(result) } } + fun safeExecuter( codeBlock: suspend () -> ChargebeeResult, completion: (ChargebeeResult) -> Unit, @@ -48,25 +49,46 @@ internal class ResultHandler { ChargebeeResult.Error(ex) } catch (ex: UnknownHostException) { print("failed: ${ex.message}") - ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 502))) + ChargebeeResult.Error( + exp = CBException( + ErrorDetail( + ex.message, + httpStatusCode = 502 + ) + ) + ) } catch (ex: Exception) { try { logger?.error(ex.message ?: "failed") } catch (ex: Exception) { print("Exception : ${ex.message}") } - ChargebeeResult.Error(exp = CBException(ErrorDetail(ex.message, httpStatusCode = 502))) + ChargebeeResult.Error( + exp = CBException( + ErrorDetail( + ex.message, + httpStatusCode = 502 + ) + ) + ) } completion(result) } - }catch (exp: Exception){ + } catch (exp: Exception) { print("failed: ${exp.message}") ChargebeeResult.Error(exp = CBException(ErrorDetail(exp.message))) } } - private fun coroutineExceptionHandler() : CoroutineExceptionHandler { - val coroutineExceptionHandler = CoroutineExceptionHandler{_, throwable -> + + fun safeExecute(codeBlock: suspend () -> Any) { + CoroutineScope(Dispatchers.IO).launch { + codeBlock() + } + } + + private fun coroutineExceptionHandler(): CoroutineExceptionHandler { + val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> print("CoroutineExceptionHandler : ${throwable.message}") } return coroutineExceptionHandler diff --git a/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt b/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt index 0088493..82ad5cf 100644 --- a/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt +++ b/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt @@ -9,20 +9,29 @@ import com.chargebee.android.repository.LoggerRepository internal class LoggerResource: BaseResource(Chargebee.baseUrl) { - suspend fun log(action: String, type: LogType, errorMessage: String? = null, errorCode: Int? = null, deviceModelName: String? = null, - platform: String? = null, - osVersion: String? = null, - sdkVersion: String? = null): CBResult { - var data = logData(action, type, errorMessage, errorCode,deviceModelName,platform, osVersion, sdkVersion) + suspend fun log( + action: String, type: LogType, errorMessage: String? = null, errorCode: Int? = null, deviceModelName: String? = null, + platform: String? = null, + osVersion: String? = null, + sdkVersion: String? = null, + additionalInfo: Map? = null): CBResult { + var data = logData(action, type, errorMessage, errorCode,deviceModelName,platform, osVersion, sdkVersion, additionalInfo) val logDetail = LogDetail(data = data) apiClient.create(LoggerRepository::class.java).log(logDetail = logDetail) return Success(null) } - private fun logData(action: String, type: LogType, errorMessage: String?, errorCode: Int?,deviceModelName: String? = null, - platform: String? = null, - osVersion: String? = null, - sdkVersion: String? = null): MutableMap { + private fun logData( + action: String, + type: LogType, + errorMessage: String?, + errorCode: Int?, + deviceModelName: String? = null, + platform: String? = null, + osVersion: String? = null, + sdkVersion: String? = null, + additionalInfo: Map? = null + ): MutableMap { var data = mutableMapOf( "key" to "cb.logging", "ref_module" to Chargebee.environment, @@ -36,6 +45,7 @@ internal class LoggerResource: BaseResource(Chargebee.baseUrl) { ) errorMessage?.let { data["error_message"] = it } errorCode?.let { data["error_code"] = "$it" } + additionalInfo?.let {"additional_info" to it } Log.i(javaClass.simpleName, "logData :$data") return data } From 15dc0a371d78af979f881bd30840afbf50d3856a Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 11 Jul 2023 21:04:34 +0530 Subject: [PATCH 33/84] Invoke showManageSubscriptions settings screen from SDK --- .../com/chargebee/example/MainActivity.kt | 2 ++ .../java/com/chargebee/example/util/CBMenu.kt | 3 ++- .../java/com/chargebee/android/Chargebee.kt | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index d98d565..0821ba2 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -135,6 +135,8 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { CBMenu.RestorePurchase.value -> { restorePurchases() } + CBMenu.ManageSubscription.value -> + Chargebee.showManageSubscriptionsSettings(context = this, productId = "chargebee.pro.mobile",packageName = this.packageName) else -> { Log.i(javaClass.simpleName, " Not implemented") } diff --git a/app/src/main/java/com/chargebee/example/util/CBMenu.kt b/app/src/main/java/com/chargebee/example/util/CBMenu.kt index 41b3bab..777dc73 100644 --- a/app/src/main/java/com/chargebee/example/util/CBMenu.kt +++ b/app/src/main/java/com/chargebee/example/util/CBMenu.kt @@ -13,6 +13,7 @@ enum class CBMenu(val value: String) { SubsStatus("Get Subscription Status"), SubsList("Get Subscriptions List"), GetEntitlements("Get Entitlements"), - RestorePurchase("Restore Purchase") + RestorePurchase("Restore Purchase"), + ManageSubscription("Manage Subscriptions") } diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 67ff6cb..f1ac447 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -1,5 +1,9 @@ package com.chargebee.android +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.core.content.ContextCompat import android.text.TextUtils import android.util.Log import com.chargebee.android.BuildConfig @@ -34,6 +38,9 @@ object Chargebee { const val platform: String = "Android" const val sdkVersion: String = BuildConfig.VERSION_NAME const val limit: String = "100" + private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" + private const val SUBSCRIPTION_URL + = "https://play.google.com/store/account/subscriptions?sku=%s&package=%s" /* Configure the app details with chargebee system */ fun configure(site: String, publishableApiKey: String, allowErrorLogging: Boolean = true, sdkKey: String="", packageName: String="" ) { @@ -230,4 +237,22 @@ object Chargebee { Success(cbTempToken) }, completion, logger) } + + fun showManageSubscriptionsSettings( + context: Context, + productId: String = "", + packageName: String = "" + ) { + val uriString = if (productId.isNotEmpty() && packageName.isNotEmpty()) { + String.format( + SUBSCRIPTION_URL, + productId, packageName + ); + } else { + PLAY_STORE_SUBSCRIPTION_URL + } + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(uriString) + ContextCompat.startActivity(context, intent, null) + } } \ No newline at end of file From c6c1257c37ba8f967c7f826327cce530676615b0 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Wed, 12 Jul 2023 11:39:53 +0530 Subject: [PATCH 34/84] docs: updates example app --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b41dcc..5338d5c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,6 @@ Chargebee Success! The token is : Ok - Subscribe - Subscribed + Buy + Purchased \ No newline at end of file From 8b5e89a682cb1398573b7a58f7b7e45c446afe79 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 12 Jul 2023 12:23:12 +0530 Subject: [PATCH 35/84] Updated comment lines --- chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index f1ac447..634a71a 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -238,6 +238,13 @@ object Chargebee { }, completion, logger) } + /** + * This method will be used to show the Manage Subscriptions Settings in your App, + * + * @param [context] Current activity context + * @param [productId] Optional. Product Identifier. + * @param [packageName] Optional. Application Id. + */ fun showManageSubscriptionsSettings( context: Context, productId: String = "", From c8977d6637219915a48537513c24092e709ee09f Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 12 Jul 2023 14:23:50 +0530 Subject: [PATCH 36/84] Adds intent flags for stacks --- chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 634a71a..c2b025f 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -2,6 +2,7 @@ package com.chargebee.android import android.content.Context import android.content.Intent +import android.content.Intent.* import android.net.Uri import androidx.core.content.ContextCompat import android.text.TextUtils @@ -260,6 +261,7 @@ object Chargebee { } val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(uriString) + intent.flags = FLAG_ACTIVITY_NEW_TASK ContextCompat.startActivity(context, intent, null) } } \ No newline at end of file From 032102b256595d7d62a7f07fd6627ccdf485e1bc Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 12 Jul 2023 14:33:59 +0530 Subject: [PATCH 37/84] remove un-used imports --- chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index c2b025f..1d3a018 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -7,8 +7,6 @@ import android.net.Uri import androidx.core.content.ContextCompat 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 From cb31fa4e4df2fd8f74ca57e96535e2daece0f70a Mon Sep 17 00:00:00 2001 From: haripriyan Date: Wed, 12 Jul 2023 15:59:30 +0530 Subject: [PATCH 38/84] fix: adds customer and product ids to log --- .../java/com/chargebee/android/billingservice/CBPurchase.kt | 2 +- .../src/main/java/com/chargebee/android/loggers/CBLogger.kt | 3 ++- .../java/com/chargebee/android/resources/LoggerResource.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) 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 585efb4..4bd9105 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -391,7 +391,7 @@ object CBPurchase { } private fun additionalInfo(customer: CBCustomer?, product: CBProduct, productType: OneTimeProductType? = null): Map { val map = mutableMapOf("product" to product.productId) - customer?.let { "customerId" to (it.id ?: "") } + customer?.let { map["customerId"] = (it.id ?: "") } productType?.let { map["productType"] = it.toString() } return map } diff --git a/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt b/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt index 4a9aebd..64684d1 100644 --- a/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt +++ b/chargebee/src/main/java/com/chargebee/android/loggers/CBLogger.kt @@ -35,7 +35,8 @@ class CBLogger(private val name: String, deviceModelName, platform, osVersion, - sdkVersion + sdkVersion, + additionalInfo ) } } diff --git a/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt b/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt index 82ad5cf..b0a093d 100644 --- a/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt +++ b/chargebee/src/main/java/com/chargebee/android/resources/LoggerResource.kt @@ -45,7 +45,7 @@ internal class LoggerResource: BaseResource(Chargebee.baseUrl) { ) errorMessage?.let { data["error_message"] = it } errorCode?.let { data["error_code"] = "$it" } - additionalInfo?.let {"additional_info" to it } + additionalInfo?.let { data.putAll(it) } Log.i(javaClass.simpleName, "logData :$data") return data } From b9769ea0d66e0624c8543f65233f16048f4dea62 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 12 Jul 2023 17:38:44 +0530 Subject: [PATCH 39/84] Updated with pr comments and readme file --- README.md | 3 +++ chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8e6e0be..1d363ef 100644 --- a/README.md +++ b/README.md @@ -388,6 +388,9 @@ Once your customer’s card data is processed and stored, and a Chargebee token Please refer to the [Chargebee API Docs](https://apidocs.chargebee.com/docs/api) for subsequent integration steps. +### Invoke Manage Subscriptions in your App +The `showManageSubscriptionsSettings()` function is designed to invoke the Manage Subscriptions in your app using Chargebee's Android SDKs. `Chargebee.showManageSubscriptionsSettings()`, opens the Play Store App subscriptions settings page. + ## License Chargebee is available under the [MIT license](https://opensource.org/licenses/MIT). See the LICENSE file for more info. diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 1d3a018..94749ea 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -246,10 +246,10 @@ object Chargebee { */ fun showManageSubscriptionsSettings( context: Context, - productId: String = "", - packageName: String = "" + productId: String? = "", + packageName: String? = "" ) { - val uriString = if (productId.isNotEmpty() && packageName.isNotEmpty()) { + val uriString = if (productId?.isNotEmpty() == true && packageName?.isNotEmpty() == true) { String.format( SUBSCRIPTION_URL, productId, packageName From 2171f0431fbe7c1a1f3d842d6d57194b9f2d7320 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 12 Jul 2023 18:18:56 +0530 Subject: [PATCH 40/84] Refactored code with suggested and tested --- .../src/main/java/com/chargebee/android/Chargebee.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 94749ea..aab3465 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -246,16 +246,16 @@ object Chargebee { */ fun showManageSubscriptionsSettings( context: Context, - productId: String? = "", - packageName: String? = "" + productId: String? = null, + packageName: String? = null ) { - val uriString = if (productId?.isNotEmpty() == true && packageName?.isNotEmpty() == true) { + val uriString = if (productId == null && packageName == null) { + PLAY_STORE_SUBSCRIPTION_URL + } else { String.format( SUBSCRIPTION_URL, productId, packageName ); - } else { - PLAY_STORE_SUBSCRIPTION_URL } val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(uriString) From a64ae0aa3f525d9d6ec44dc6bce3e76d6651586d Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 13 Jul 2023 16:39:56 +0530 Subject: [PATCH 41/84] Version bump and updated README.md --- README.md | 4 ++-- chargebee/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ac512ae..e60495c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.19' +implementation 'com.chargebee:chargebee-android:1.0.20' ``` ## Example project @@ -470,7 +470,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.19' + implementation 'com.chargebee:chargebee-android:1.0.20' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index fadcaad..76a9953 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.19" + versionName "1.0.20" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From c3e47005e403bc3bb550ea6dd535f568f3bcaf15 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Fri, 14 Jul 2023 10:35:20 +0530 Subject: [PATCH 42/84] code format issue --- .../java/com/chargebee/android/Chargebee.kt | 113 ++++++++++++------ 1 file changed, 79 insertions(+), 34 deletions(-) diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index aab3465..5ecf590 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -37,12 +37,19 @@ object Chargebee { const val platform: String = "Android" const val sdkVersion: String = BuildConfig.VERSION_NAME const val limit: String = "100" - private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" - private const val SUBSCRIPTION_URL - = "https://play.google.com/store/account/subscriptions?sku=%s&package=%s" + private const val PLAY_STORE_SUBSCRIPTION_URL = + "https://play.google.com/store/account/subscriptions" + private const val SUBSCRIPTION_URL = + "https://play.google.com/store/account/subscriptions?sku=%s&package=%s" /* Configure the app details with chargebee system */ - fun configure(site: String, publishableApiKey: String, allowErrorLogging: Boolean = true, sdkKey: String="", packageName: String="" ) { + fun configure( + site: String, + publishableApiKey: String, + allowErrorLogging: Boolean = true, + sdkKey: String = "", + packageName: String = "" + ) { this.applicationId = packageName this.publishableApiKey = publishableApiKey this.site = site @@ -50,11 +57,11 @@ object Chargebee { this.baseUrl = "https://${site}.chargebee.com/api/" this.allowErrorLogging = allowErrorLogging this.sdkKey = sdkKey - val auth = Auth(sdkKey,applicationId,appName, channel) + val auth = Auth(sdkKey, applicationId, appName, channel) CBAuthentication.authenticate(auth) { - when(it){ - is ChargebeeResult.Success ->{ + when (it) { + is ChargebeeResult.Success -> { Log.i(javaClass.simpleName, "Environment Setup Completed") Log.i(javaClass.simpleName, " Response :${it.data}") val response = it.data as CBAuthResponse @@ -62,7 +69,7 @@ object Chargebee { this.applicationId = response.in_app_detail.app_id this.appName = response.in_app_detail.app_name } - is ChargebeeResult.Error ->{ + is ChargebeeResult.Error -> { Log.i(javaClass.simpleName, "Exception from server :${it.exp.message}") this.version = CatalogVersion.Unknown.value } @@ -112,45 +119,69 @@ object Chargebee { @Throws(InvalidRequestException::class, OperationFailedException::class) fun retrieveSubscription(subscriptionId: String, completion: (ChargebeeResult) -> Unit) { val logger = CBLogger(name = "Subscription", action = "Fetch Subscription") - ResultHandler.safeExecuter({ SubscriptionResource().retrieveSubscription(subscriptionId) }, completion, logger) + ResultHandler.safeExecuter( + { SubscriptionResource().retrieveSubscription(subscriptionId) }, + completion, + logger + ) } + /* Get the subscriptions list from chargebee system by using Customer Id */ @Throws(InvalidRequestException::class, OperationFailedException::class) - fun retrieveSubscriptions(queryParams: Map = mapOf(), completion: (ChargebeeResult) -> Unit) { - val logger = CBLogger(name = "Subscription", action = "Fetch Subscription by using CustomerId") + fun retrieveSubscriptions( + queryParams: Map = mapOf(), + completion: (ChargebeeResult) -> Unit + ) { + val logger = + CBLogger(name = "Subscription", action = "Fetch Subscription by using CustomerId") if (queryParams.isNotEmpty()) { - ResultHandler.safeExecuter( - { SubscriptionResource().retrieveSubscriptions(queryParams) }, - completion, - logger - ) - }else{ - completion(ChargebeeResult.Error( - exp = CBException( - error = ErrorDetail(message = "Array/Query Param is empty", apiErrorCode = "400", httpStatusCode = 400) + ResultHandler.safeExecuter( + { SubscriptionResource().retrieveSubscriptions(queryParams) }, + completion, + logger + ) + } else { + completion( + ChargebeeResult.Error( + exp = CBException( + error = ErrorDetail( + message = "Array/Query Param is empty", + apiErrorCode = "400", + httpStatusCode = 400 + ) + ) ) - )) + ) } } /* Get the Plan details from chargebee system */ @Throws(InvalidRequestException::class, OperationFailedException::class) - fun retrievePlan(planId: String, completion : (ChargebeeResult) -> Unit) { + fun retrievePlan(planId: String, completion: (ChargebeeResult) -> Unit) { val logger = CBLogger(name = "plan", action = "getPlan") if (TextUtils.isEmpty(planId)) - completion(ChargebeeResult.Error( - exp = CBException( - error = ErrorDetail(message = "Plan ID is empty", apiErrorCode = "400", httpStatusCode = 400) + completion( + ChargebeeResult.Error( + exp = CBException( + error = ErrorDetail( + message = "Plan ID is empty", + apiErrorCode = "400", + httpStatusCode = 400 + ) + ) ) - )) + ) else ResultHandler.safeExecuter({ PlanResource().retrievePlan(planId) }, completion, logger) } /* Get the list of Plan's from chargebee system */ @Throws(InvalidRequestException::class, OperationFailedException::class) - fun retrieveAllPlans(params: Array = arrayOf(), completion : (ChargebeeResult) -> Unit) { + fun retrieveAllPlans( + params: Array = arrayOf(), + completion: (ChargebeeResult) -> Unit + ) { val logger = CBLogger(name = "plans", action = "getAllPlan") if (params.isNotEmpty()) { ResultHandler.safeExecuter( @@ -196,14 +227,20 @@ object Chargebee { /* Get the Item details from chargebee system */ @Throws(InvalidRequestException::class, OperationFailedException::class) - fun retrieveItem(itemId: String, completion : (ChargebeeResult) -> Unit) { + fun retrieveItem(itemId: String, completion: (ChargebeeResult) -> Unit) { val logger = CBLogger(name = "item", action = "getItem") if (TextUtils.isEmpty(itemId)) - completion(ChargebeeResult.Error( - exp = CBException( - error = ErrorDetail(message = "Item ID is empty", apiErrorCode = "400", httpStatusCode = 400) + completion( + ChargebeeResult.Error( + exp = CBException( + error = ErrorDetail( + message = "Item ID is empty", + apiErrorCode = "400", + httpStatusCode = 400 + ) + ) ) - )) + ) else ResultHandler.safeExecuter({ ItemsResource().retrieveItem(itemId) }, completion, logger) } @@ -212,7 +249,11 @@ object Chargebee { @Throws(InvalidRequestException::class, OperationFailedException::class) fun retrieveEntitlements(subscriptionId: String, completion: (ChargebeeResult) -> Unit) { val logger = CBLogger(name = "Entitlements", action = "retrieve_entitlements") - ResultHandler.safeExecuter({ EntitlementsResource().retrieveEntitlements(subscriptionId) }, completion, logger) + ResultHandler.safeExecuter( + { EntitlementsResource().retrieveEntitlements(subscriptionId) }, + completion, + logger + ) } @Throws(InvalidRequestException::class, OperationFailedException::class) @@ -221,7 +262,11 @@ object Chargebee { ResultHandler.safeExecute({ AddonResource().retrieve(addonId) }, handler, logger) } - @Throws(InvalidRequestException::class, OperationFailedException::class, PaymentException::class) + @Throws( + InvalidRequestException::class, + OperationFailedException::class, + PaymentException::class + ) fun createTempToken(detail: PaymentDetail, completion: (CBResult) -> Unit) { val logger = CBLogger(name = "cb_temp_token", action = "create_temp_token") ResultHandler.safeExecute({ From 1829be8271d3ac6f55f4a4ade23095c9627e8061 Mon Sep 17 00:00:00 2001 From: cb-sabuj Date: Mon, 17 Jul 2023 19:57:55 +0530 Subject: [PATCH 43/84] OMNISUB-5544-Invoke Manage Subscriptions in your App --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e60495c..40a1c4a 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,9 @@ 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. +### Invoke Manage Subscriptions in your App +The `showManageSubscriptionsSettings()` function is designed to invoke the Manage Subscriptions in your app using Chargebee's Android SDKs. `Chargebee.showManageSubscriptionsSettings()`, opens the Play Store App subscriptions settings page. + ### 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`. @@ -429,8 +432,6 @@ Once your customer’s card data is processed and stored, and a Chargebee token Please refer to the [Chargebee API Docs](https://apidocs.chargebee.com/docs/api) for subsequent integration steps. -### Invoke Manage Subscriptions in your App -The `showManageSubscriptionsSettings()` function is designed to invoke the Manage Subscriptions in your app using Chargebee's Android SDKs. `Chargebee.showManageSubscriptionsSettings()`, opens the Play Store App subscriptions settings page. ## License From 38c18c67ade35c6d8fb55a6c71f317da93b35c6b Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 16 Aug 2023 17:05:05 +0530 Subject: [PATCH 44/84] fix: adds customer object on restore --- README.md | 4 +++- .../java/com/chargebee/example/billing/BillingViewModel.kt | 3 ++- .../java/com/chargebee/android/billingservice/CBPurchase.kt | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40a1c4a..bf65ee7 100644 --- a/README.md +++ b/README.md @@ -176,8 +176,10 @@ The `restorePurchases()` function helps to recover your app user's previous purc To retrieve **inactive** purchases along with the **active** purchases for your app user, you can call the `restorePurchases()` function with the `includeInActiveProducts` parameter set to `true`. If you only want to restore active subscriptions, set the parameter to `false`. Here is an example of how to use the `restorePurchases()` function in your code with the `includeInActiveProducts` parameter set to `true`. +`CBCustomer` - **Optional object**. Although this is an optional object, we recommend passing the necessary customer details, such as `customerId`, `firstName`, `lastName`, and `email` if it is available before the user subscribes to your App. This ensures that the customer details in your database match the customer details in Chargebee. If the `customerId` is not passed in the customer's details, then the value of `customerId` will be the same as the `SubscriptionId` created in Chargebee. + ```kotlin -CBPurchase.restorePurchases(context = current activity context, includeInActivePurchases = false, object : CBCallback.RestorePurchaseCallback{ +CBPurchase.restorePurchases(context = current activity context, customer = CBCustomer, includeInActivePurchases = false, object : CBCallback.RestorePurchaseCallback{ override fun onSuccess(result: List) { result.forEach { Log.i(javaClass.simpleName, "Successfully restored purchases") 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 a72e43d..1c66c00 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -242,8 +242,9 @@ class BillingViewModel : ViewModel() { } fun restorePurchases(context: Context, includeInActivePurchases: Boolean = false) { + val customer = CBCustomer("test-restore","","","") CBPurchase.restorePurchases( - context = context, includeInActivePurchases = includeInActivePurchases, + context = context, customer = customer, includeInActivePurchases = includeInActivePurchases, completionCallback = object : CBCallback.RestorePurchaseCallback { override fun onSuccess(result: List) { result.forEach { 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 4bd9105..d839966 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -151,16 +151,19 @@ object CBPurchase { * And the associated purchases will be synced with Chargebee. * * @param [context] Current activity context + * @param [customer] Optional. Customer Object. * @param [includeInActivePurchases] False by default. if true, only active purchases restores and synced with Chargebee. * @param [completionCallback] The listener will be called when restore purchase completes. */ @JvmStatic fun restorePurchases( context: Context, + customer: CBCustomer? = null, includeInActivePurchases: Boolean = false, completionCallback: CBCallback.RestorePurchaseCallback ) { this.includeInActivePurchases = includeInActivePurchases + this.customer = customer sharedInstance(context).restorePurchases(completionCallback) } From 5e606110fc7877645b57a555ca4ed5b542ff93f6 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 17 Aug 2023 09:31:07 +0530 Subject: [PATCH 45/84] version bump and readme updates --- README.md | 4 ++-- chargebee/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bf65ee7..599050e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.20' +implementation 'com.chargebee:chargebee-android:1.0.21' ``` ## Example project @@ -473,7 +473,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.20' + implementation 'com.chargebee:chargebee-android:1.0.21' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 76a9953..1d0961b 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.20" + versionName "1.0.21" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From 4b9a03088d753977885e46d53016713c14e91874 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 17 Aug 2023 12:29:52 +0530 Subject: [PATCH 46/84] updates readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 599050e..66e989f 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ The `restorePurchases()` function helps to recover your app user's previous purc To retrieve **inactive** purchases along with the **active** purchases for your app user, you can call the `restorePurchases()` function with the `includeInActiveProducts` parameter set to `true`. If you only want to restore active subscriptions, set the parameter to `false`. Here is an example of how to use the `restorePurchases()` function in your code with the `includeInActiveProducts` parameter set to `true`. -`CBCustomer` - **Optional object**. Although this is an optional object, we recommend passing the necessary customer details, such as `customerId`, `firstName`, `lastName`, and `email` if it is available before the user subscribes to your App. This ensures that the customer details in your database match the customer details in Chargebee. If the `customerId` is not passed in the customer's details, then the value of `customerId` will be the same as the `SubscriptionId` created in Chargebee. +`CBCustomer` - **Optional object**. Although this is an optional object, we recommend passing the necessary customer details, such as `customerId`, `firstName`, `lastName`, and `email` if it is available before the user subscribes to your App. This ensures that the customer details in your database match the customer details in Chargebee. If the `customerId` is not passed in the customer's details, then the value of `customerId` will be the same as the `subscriptionId` created in Chargebee. Also, the restored subscriptions will not be associate with existing customerId. ```kotlin CBPurchase.restorePurchases(context = current activity context, customer = CBCustomer, includeInActivePurchases = false, object : CBCallback.RestorePurchaseCallback{ From 4d65d80484f43a2320a59022e0b8a86ab42d35bd Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 17 Aug 2023 14:59:45 +0530 Subject: [PATCH 47/84] updates readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 66e989f..49b9068 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ 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. From 6cf7b3bc3c3fb7f387e03dc805f4db240e6158d4 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Fri, 1 Sep 2023 10:02:16 +0530 Subject: [PATCH 48/84] fix: fixes error when trying to valiate a non-purchased item --- .../billingservice/BillingClientManager.kt | 34 ++++++++++++++++--- 1 file changed, 30 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 b827ac7..ce7912b 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -398,6 +398,13 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) ) + private fun itemNotOwnedException(): CBException { + return CBException(ErrorDetail( + message = BillingErrorCode.ITEM_NOT_OWNED.message, + httpStatusCode = BillingErrorCode.ITEM_NOT_OWNED.code + )) + } + private fun queryPurchaseHistoryFromStore( connectionStatus: Boolean ) { @@ -517,7 +524,10 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } - internal fun validateReceiptWithChargebee(product: CBProduct, completionCallback: CBCallback.PurchaseCallback ) { + internal fun validateReceiptWithChargebee( + product: CBProduct, + completionCallback: CBCallback.PurchaseCallback + ) { this.purchaseCallBack = completionCallback onConnected({ status -> if (status) @@ -525,7 +535,13 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { val purchaseTransaction = purchaseHistoryList.filter { it.productId.first() == product.productId } - validateReceipt(purchaseTransaction.first().purchaseToken, product) + val transaction = purchaseTransaction.firstOrNull() + transaction?.let { + validateReceipt(transaction.purchaseToken, product) + } ?: run { + completionCallback.onError(itemNotOwnedException()) + } + } else completionCallback.onError( connectionError @@ -582,14 +598,24 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } - internal fun validateNonSubscriptionReceiptWithChargebee(product: CBProduct, completionCallback: CBCallback.OneTimePurchaseCallback) { + internal fun validateNonSubscriptionReceiptWithChargebee( + product: CBProduct, + completionCallback: CBCallback.OneTimePurchaseCallback + ) { + this.oneTimePurchaseCallback = completionCallback onConnected({ status -> if (status) queryPurchaseHistory { purchaseHistoryList -> val purchaseTransaction = purchaseHistoryList.filter { it.productId.first() == product.productId } - validateNonSubscriptionReceipt(purchaseTransaction.first().purchaseToken, product) + val transaction = purchaseTransaction.firstOrNull() + transaction?.let { + validateNonSubscriptionReceipt(transaction.purchaseToken, product) + } ?: run { + completionCallback.onError(itemNotOwnedException()) + } + } else completionCallback.onError( connectionError From 8e18a5e8adbe5ad7c4dfb2d69162f1e55e12b993 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Fri, 1 Sep 2023 10:34:20 +0530 Subject: [PATCH 49/84] build: updates release version --- README.md | 4 ++-- chargebee/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 49b9068..170d362 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.21' +implementation 'com.chargebee:chargebee-android:1.0.22' ``` ## Example project @@ -474,7 +474,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.21' + implementation 'com.chargebee:chargebee-android:1.0.22' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 1d0961b..252996f 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.21" + versionName "1.0.22" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From a93bb37ad60478c211a383bd57aa190ee7f99887 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Tue, 5 Sep 2023 18:35:00 +0530 Subject: [PATCH 50/84] fix: release build code optimize --- app/build.gradle | 2 +- chargebee/build.gradle | 2 +- chargebee/consumer-rules.pro | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 85be433..d1613a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 252996f..16827eb 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -17,7 +17,7 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/chargebee/consumer-rules.pro b/chargebee/consumer-rules.pro index e69de29..83dc4b3 100644 --- a/chargebee/consumer-rules.pro +++ b/chargebee/consumer-rules.pro @@ -0,0 +1,2 @@ +# keep the classes when optimizing the code +-keep class com.chargebee.android.** { *;} \ No newline at end of file From 12f822723abcd8d1e92a9db303760ef30e3638af Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 6 Sep 2023 10:27:07 +0530 Subject: [PATCH 51/84] version bump --- README.md | 2 +- chargebee/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 170d362..c3e9450 100644 --- a/README.md +++ b/README.md @@ -474,7 +474,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.22' + implementation 'com.chargebee:chargebee-android:1.0.23' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 16827eb..d1c2437 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.22" + versionName "1.0.23" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From 4f6765210f355173ea035103c52895f988111189 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 6 Sep 2023 12:25:39 +0530 Subject: [PATCH 52/84] updates README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3e9450..62a20a6 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.22' +implementation 'com.chargebee:chargebee-android:1.0.23' ``` ## Example project From c5c4aeee029aed1bd2aaa217c18b2b07ba18dc69 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 6 Sep 2023 15:26:24 +0530 Subject: [PATCH 53/84] Reverted progaurd rules from sdk and updated the version --- README.md | 4 ++-- chargebee/build.gradle | 4 ++-- chargebee/consumer-rules.pro | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c3e9450..a20e3c9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.22' +implementation 'com.chargebee:chargebee-android:1.0.24' ``` ## Example project @@ -474,7 +474,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.23' + implementation 'com.chargebee:chargebee-android:1.0.24' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index d1c2437..1ad60b6 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.23" + versionName "1.0.24" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -17,7 +17,7 @@ android { buildTypes { release { - minifyEnabled true + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/chargebee/consumer-rules.pro b/chargebee/consumer-rules.pro index 83dc4b3..6f7d72e 100644 --- a/chargebee/consumer-rules.pro +++ b/chargebee/consumer-rules.pro @@ -1,2 +1,2 @@ # keep the classes when optimizing the code --keep class com.chargebee.android.** { *;} \ No newline at end of file +#-keep class com.chargebee.android.** { *;} \ No newline at end of file From 71f72bc3ddecec3f7b6821a8b82b6432e5abd2b2 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Wed, 6 Sep 2023 15:51:01 +0530 Subject: [PATCH 54/84] updates README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 38d2b22..a20e3c9 100644 --- a/README.md +++ b/README.md @@ -474,7 +474,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.23' + implementation 'com.chargebee:chargebee-android:1.0.24' ``` Example project --------------- From 107d57a624dc714f7859b33b54552b6d0d924c20 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Thu, 7 Sep 2023 10:20:47 +0530 Subject: [PATCH 55/84] adds proguard rule --- README.md | 4 ++-- chargebee/build.gradle | 2 +- chargebee/consumer-rules.pro | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a20e3c9..fadd420 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.24' +implementation 'com.chargebee:chargebee-android:1.0.25' ``` ## Example project @@ -474,7 +474,7 @@ Chargebee is available under the [MIT license](https://opensource.org/licenses/M To install Chargebee's Android SDK, add the following dependency to the build.gradle file. ``` - implementation 'com.chargebee:chargebee-android:1.0.24' + implementation 'com.chargebee:chargebee-android:1.0.25' ``` Example project --------------- diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 1ad60b6..a186d1a 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.24" + versionName "1.0.25" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/chargebee/consumer-rules.pro b/chargebee/consumer-rules.pro index 6f7d72e..83dc4b3 100644 --- a/chargebee/consumer-rules.pro +++ b/chargebee/consumer-rules.pro @@ -1,2 +1,2 @@ # keep the classes when optimizing the code -#-keep class com.chargebee.android.** { *;} \ No newline at end of file +-keep class com.chargebee.android.** { *;} \ No newline at end of file From 8e8ca9639843fe5121080dadad485305c9c317b1 Mon Sep 17 00:00:00 2001 From: Venkatesh Ravichandran Date: Thu, 5 Oct 2023 14:43:25 +0530 Subject: [PATCH 56/84] Changing productsList to mutableSet to handle Billing library 5 format plans --- .../com/chargebee/android/billingservice/CBPurchase.kt | 9 +++++---- .../chargebee/android/exceptions/CBProductIDResult.kt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) 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 d839966..fafb90b 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -16,7 +16,7 @@ import com.chargebee.android.resources.ReceiptResource object CBPurchase { private var billingClientManager: BillingClientManager? = null - val productIdList = arrayListOf() + val productIdList = mutableSetOf() private var customer: CBCustomer? = null internal var includeInActivePurchases = false internal var productType = OneTimeProductType.UNKNOWN @@ -27,7 +27,7 @@ object CBPurchase { @JvmStatic fun retrieveProductIdentifers( params: Array = arrayOf(), - completion: (CBProductIDResult>) -> Unit + completion: (CBProductIDResult>) -> Unit ) { if (params.isNotEmpty()) { params[0] = params[0].ifEmpty { Chargebee.limit } @@ -286,7 +286,7 @@ object CBPurchase { */ internal fun retrieveProductIDList( params: Array, - completion: (CBProductIDResult>) -> Unit + completion: (CBProductIDResult>) -> Unit ) { // The Plan will be fetched based on the user catalog versions in chargebee system. when (Chargebee.version) { @@ -326,7 +326,8 @@ object CBPurchase { val productsList = (it.data as ItemsWrapper).list productIdList.clear() for (item in productsList) { - productIdList.add(item.item.id) + val id = item.item.id.split("-") + productIdList.add(id[0]) } completion(CBProductIDResult.ProductIds(productIdList)) } diff --git a/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt b/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt index d60efdd..91d3339 100644 --- a/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt +++ b/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt @@ -3,6 +3,6 @@ package com.chargebee.android.exceptions import java.util.ArrayList sealed class CBProductIDResult { - data class ProductIds(val IDs: ArrayList) : CBProductIDResult>() + data class ProductIds(val IDs: MutableSet) : CBProductIDResult>() data class Error(val exp: CBException): CBProductIDResult() } From 67e5999200a8fbf3dca31e1c908e76fa3bf79b28 Mon Sep 17 00:00:00 2001 From: Venkatesh Ravichandran Date: Thu, 5 Oct 2023 16:06:17 +0530 Subject: [PATCH 57/84] Changing the API contract to existing type --- .../com/chargebee/android/billingservice/CBPurchase.kt | 8 ++++---- .../com/chargebee/android/exceptions/CBProductIDResult.kt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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 fafb90b..34e871f 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -27,7 +27,7 @@ object CBPurchase { @JvmStatic fun retrieveProductIdentifers( params: Array = arrayOf(), - completion: (CBProductIDResult>) -> Unit + completion: (CBProductIDResult>) -> Unit ) { if (params.isNotEmpty()) { params[0] = params[0].ifEmpty { Chargebee.limit } @@ -286,7 +286,7 @@ object CBPurchase { */ internal fun retrieveProductIDList( params: Array, - completion: (CBProductIDResult>) -> Unit + completion: (CBProductIDResult>) -> Unit ) { // The Plan will be fetched based on the user catalog versions in chargebee system. when (Chargebee.version) { @@ -305,7 +305,7 @@ object CBPurchase { } } - completion(CBProductIDResult.ProductIds(productIdList)) + completion(CBProductIDResult.ProductIds(ArrayList(productIdList))) } is ChargebeeResult.Error -> { Log.e( @@ -329,7 +329,7 @@ object CBPurchase { val id = item.item.id.split("-") productIdList.add(id[0]) } - completion(CBProductIDResult.ProductIds(productIdList)) + completion(CBProductIDResult.ProductIds(ArrayList(productIdList))) } is ChargebeeResult.Error -> { Log.e( diff --git a/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt b/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt index 91d3339..d60efdd 100644 --- a/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt +++ b/chargebee/src/main/java/com/chargebee/android/exceptions/CBProductIDResult.kt @@ -3,6 +3,6 @@ package com.chargebee.android.exceptions import java.util.ArrayList sealed class CBProductIDResult { - data class ProductIds(val IDs: MutableSet) : CBProductIDResult>() + data class ProductIds(val IDs: ArrayList) : CBProductIDResult>() data class Error(val exp: CBException): CBProductIDResult() } From e6a14308136e9d7f98f5d1e426011a55c83ce2af Mon Sep 17 00:00:00 2001 From: Venkatesh Ravichandran Date: Thu, 5 Oct 2023 17:33:20 +0530 Subject: [PATCH 58/84] Version change in build.gradle --- chargebee/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chargebee/build.gradle b/chargebee/build.gradle index a186d1a..838961e 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.0.25" + versionName "1.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From df682e0b16fcacf050c7142452852f76ae9eef0d Mon Sep 17 00:00:00 2001 From: haripriyan Date: Fri, 13 Oct 2023 16:19:49 +0530 Subject: [PATCH 59/84] Upgrades to Billing library 5.2.1 --- chargebee/build.gradle | 7 +- .../java/com/chargebee/android/Chargebee.kt | 2 +- .../billingservice/BillingClientManager.kt | 176 ++++++++++++------ .../com/chargebee/android/models/Products.kt | 6 +- 4 files changed, 128 insertions(+), 63 deletions(-) diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 838961e..82ac646 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -1,13 +1,12 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 1 versionName "1.1.0" @@ -42,7 +41,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // Google play billing library - implementation 'com.android.billingclient:billing-ktx:4.0.0' + implementation 'com.android.billingclient:billing-ktx:5.2.1' // AssertJ testImplementation "org.assertj:assertj-core:$assertj_version" diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 5ecf590..763ee1f 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = BuildConfig.VERSION_NAME + const val sdkVersion: String = "1.2.0" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" 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 ce7912b..b2d3666 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -25,7 +25,6 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { var mContext: Context? = context private val handler = Handler(Looper.getMainLooper()) private var purchaseCallBack: CBCallback.PurchaseCallback? = null - private val skusWithSkuDetails = arrayListOf() private val TAG = javaClass.simpleName lateinit var product: CBProduct private lateinit var restorePurchaseCallBack: CBCallback.RestorePurchaseCallback @@ -36,12 +35,12 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } internal fun retrieveProducts( - skuList: ArrayList, callBack: CBCallback.ListProductsCallback> + products: ArrayList, callBack: CBCallback.ListProductsCallback> ) { val productsList = ArrayList() - retrieveProducts(ProductType.SUBS.value, skuList, { subsProductsList -> + retrieveProducts(BillingClient.ProductType.SUBS, products, { subsProductsList -> productsList.addAll(subsProductsList) - retrieveProducts(ProductType.INAPP.value, skuList, { inAppProductsList -> + retrieveProducts(BillingClient.ProductType.INAPP, products, { inAppProductsList -> productsList.addAll(inAppProductsList) callBack.onSuccess(productsList) }, { error -> @@ -53,13 +52,13 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } internal fun retrieveProducts( - @BillingClient.SkuType skuType: String, - skuList: ArrayList, response: (ArrayList) -> Unit, + @BillingClient.ProductType productType: String, + products: ArrayList, response: (ArrayList) -> Unit, errorDetail: (CBException) -> Unit ) { onConnected({ status -> if (status) - loadProductDetails(skuType, skuList, { + loadProductDetails(productType, products, { response(it) }, { errorDetail(it) @@ -72,39 +71,73 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { errorDetail(error) }) } + /* Get the SKU/Products from Play Console */ private fun loadProductDetails( - @BillingClient.SkuType skuType: String, - skuList: ArrayList, + @BillingClient.ProductType productType: String, + products: ArrayList, response: (ArrayList) -> Unit, errorDetail: (CBException) -> Unit ) { try { - val params = SkuDetailsParams - .newBuilder() - .setSkusList(skuList) - .setType(skuType) - .build() - billingClient?.querySkuDetailsAsync( - params - ) { billingResult, skuDetailsList -> - if (billingResult.responseCode == OK && skuDetailsList != null) { + val queryProductDetails = products.map { + QueryProductDetailsParams.Product.newBuilder() + .setProductId(it) + .setProductType(productType) + .build() + } + + val productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(queryProductDetails).build() + + billingClient?.queryProductDetailsAsync( + productDetailsParams + ) { billingResult, productsDetail -> + if (billingResult.responseCode == OK && productsDetail != null) { try { - skusWithSkuDetails.clear() - for (skuProduct in skuDetailsList) { - val product = CBProduct( - skuProduct.sku, - skuProduct.title, - skuProduct.price, - skuProduct, - false, - ProductType.getProductType(skuProduct.type) - ) - skusWithSkuDetails.add(product) + val cbProductDetails = arrayListOf() + for (productDetail in productsDetail) { + val productId = productDetail.productId + val productType = ProductType.getProductType(productDetail.productType) + val productTitle = productDetail.title + + val subscriptionOfferDetails = productDetail.subscriptionOfferDetails + subscriptionOfferDetails?.forEach { + val price = it.pricingPhases?.pricingPhaseList?.first()?.formattedPrice ?: "0" + val offerToken = it.offerToken + val basePlanId = it.basePlanId + + val product = CBProduct( + productId, + productTitle, + basePlanId, + price, + productDetail, + offerToken, + false, + productType + ) + cbProductDetails.add(product) + } + + val oneTimePurchaseOfferDetails = productDetail.oneTimePurchaseOfferDetails + oneTimePurchaseOfferDetails?.let { + val price = it.formattedPrice + val product = CBProduct( + productId, + productTitle, + null, + price, + productDetail, + null, + false, + productType + ) + cbProductDetails.add(product) + } } - Log.i(TAG, "Product details :$skusWithSkuDetails") - response(skusWithSkuDetails) + Log.i(TAG, "Product details :$cbProductDetails") + response(cbProductDetails) } catch (ex: CBException) { errorDetail( CBException( @@ -135,9 +168,9 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) { this.purchaseCallBack = purchaseCallBack onConnected({ status -> - if (status) + if (status) { purchase(product) - else + } else purchaseCallBack.onError( connectionError ) @@ -146,16 +179,29 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { }) } + /* Purchase the product: Initiates the billing flow for an In-app-purchase */ private fun purchase(product: CBProduct) { this.product = product - val skuDetails = product.skuDetails + val productDetails = product.productDetails + val offerToken = product.offerToken + + val productDetailsParamsList = + listOf( + offerToken?.let { + BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productDetails) + .setOfferToken(it) + .build() + } + ) - val params = BillingFlowParams.newBuilder() - .setSkuDetails(skuDetails) - .build() + val billingFlowParams = + BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList) + .build() - billingClient?.launchBillingFlow(mContext as Activity, params) + billingClient?.launchBillingFlow(mContext as Activity, billingFlowParams) .takeIf { billingResult -> billingResult?.responseCode != OK }?.let { billingResult -> @@ -166,7 +212,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { httpStatusCode = billingResult.responseCode ) ) - if (product.skuDetails.type == ProductType.SUBS.value) { + if (ProductType.SUBS == product.productType) { purchaseCallBack?.onError( billingError ) @@ -202,6 +248,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { OK -> { return true } + FEATURE_NOT_SUPPORTED -> { return false } @@ -229,6 +276,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { Purchase.PurchaseState.PURCHASED -> { acknowledgePurchase(purchase) } + Purchase.PurchaseState.PENDING -> { purchaseCallBack?.onError( CBException( @@ -239,6 +287,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) ) } + Purchase.PurchaseState.UNSPECIFIED_STATE -> { purchaseCallBack?.onError( CBException( @@ -252,29 +301,31 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } } - else -> { - if (product.skuDetails.type == ProductType.SUBS.value) - purchaseCallBack?.onError( - throwCBException(billingResult) - ) - else - oneTimePurchaseCallback?.onError( - throwCBException(billingResult) - ) - } + + else -> { + if (product.productType == ProductType.SUBS) + purchaseCallBack?.onError( + throwCBException(billingResult) + ) + else + oneTimePurchaseCallback?.onError( + throwCBException(billingResult) + ) + } } } /* Acknowledge the Purchases */ private fun acknowledgePurchase(purchase: Purchase) { - when(product.productType){ - ProductType.SUBS -> { - isAcknowledgedPurchase(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) @@ -289,7 +340,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } - private fun isAcknowledgedPurchase(purchase: Purchase, success: () -> Unit, error: (CBException) -> Unit){ + private fun isAcknowledgedPurchase( + purchase: Purchase, + success: () -> Unit, + error: (CBException) -> Unit + ) { if (!purchase.isAcknowledged) { val params = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) @@ -312,6 +367,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) } } + else -> { error( throwCBException(billingResult) @@ -325,10 +381,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { /* Consume the Purchases */ private fun consumeAsyncPurchase(token: String) { consumePurchase(token) { billingResult, purchaseToken -> - when(billingResult.responseCode){ + when (billingResult.responseCode) { OK -> { validateNonSubscriptionReceipt(purchaseToken, product) } + else -> { oneTimePurchaseCallback?.onError( throwCBException(billingResult) @@ -379,6 +436,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) } } + is ChargebeeResult.Error -> { Log.e(TAG, "Exception from Server - validateReceipt() : ${it.exp.message}") purchaseCallBack?.onError(it.exp) @@ -459,7 +517,8 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private fun queryPurchaseHistoryAsync( productType: String, purchaseTransactionList: (List?) -> Unit ) { - billingClient?.queryPurchaseHistoryAsync(productType) { billingResult, subsHistoryList -> + val queryPurchaseHistoryParams = QueryPurchaseHistoryParams.newBuilder().setProductType(productType).build() + billingClient?.queryPurchaseHistoryAsync(queryPurchaseHistoryParams) { billingResult, subsHistoryList -> if (billingResult.responseCode == OK) { val purchaseHistoryList = subsHistoryList?.map { it.toPurchaseTransaction(productType) @@ -517,6 +576,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { Log.i(TAG, "Google Billing Setup Done!") status(true) } + else -> { connectionError(throwCBException(billingResult)) } @@ -590,8 +650,12 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { oneTimePurchaseCallback?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) } } + is ChargebeeResult.Error -> { - Log.e(TAG, "Exception from Server - validateNonSubscriptionReceipt() : ${it.exp.message}") + Log.e( + TAG, + "Exception from Server - validateNonSubscriptionReceipt() : ${it.exp.message}" + ) oneTimePurchaseCallback?.onError(it.exp) } } 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 88e63a8..ad9c608 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/Products.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/Products.kt @@ -1,13 +1,15 @@ package com.chargebee.android.models -import com.android.billingclient.api.SkuDetails +import com.android.billingclient.api.ProductDetails import com.chargebee.android.billingservice.ProductType data class CBProduct( val productId: String, val productTitle: String, + val productBasePlanId: String?, val productPrice: String, - var skuDetails: SkuDetails, + var productDetails: ProductDetails, + var offerToken: String?, var subStatus: Boolean, var productType: ProductType ) \ No newline at end of file From 0d73e1d35f8269c56b912500a14f749ad0dec239 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Mon, 16 Oct 2023 12:59:29 +0530 Subject: [PATCH 60/84] Fixes unit tests --- chargebee/build.gradle | 4 +- .../java/com/chargebee/android/Chargebee.kt | 2 +- .../billingservice/BillingClientManager.kt | 77 +++++++++++-------- .../billingclient/api/StubProductDetails.kt | 38 +++++++++ .../BillingClientManagerTest.kt | 5 +- .../android/restore/RestorePurchaseTest.kt | 8 +- 6 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 chargebee/src/test/java/com/android/billingclient/api/StubProductDetails.kt diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 82ac646..d216def 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 1 - versionName "1.1.0" + versionName "2.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -52,6 +52,8 @@ dependencies { // Mockito testImplementation 'org.mockito:mockito-core:2.23.0' + testImplementation 'org.json:json:20140107' + testImplementation 'androidx.test:core:1.2.0' testImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 763ee1f..785de24 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = "1.2.0" + const val sdkVersion: String = "2.0.0" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" 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 b2d3666..f9c4107 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -9,14 +9,13 @@ import com.android.billingclient.api.* import com.android.billingclient.api.BillingClient.BillingResponseCode.* import com.chargebee.android.ErrorDetail import com.chargebee.android.billingservice.BillingErrorCode.Companion.throwCBException -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.models.PurchaseTransaction import com.chargebee.android.network.CBReceiptResponse import com.chargebee.android.restore.CBRestorePurchaseManager -import kotlin.collections.ArrayList class BillingClientManager(context: Context) : PurchasesUpdatedListener { @@ -97,42 +96,16 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { try { val cbProductDetails = arrayListOf() for (productDetail in productsDetail) { - val productId = productDetail.productId - val productType = ProductType.getProductType(productDetail.productType) - val productTitle = productDetail.title val subscriptionOfferDetails = productDetail.subscriptionOfferDetails subscriptionOfferDetails?.forEach { - val price = it.pricingPhases?.pricingPhaseList?.first()?.formattedPrice ?: "0" - val offerToken = it.offerToken - val basePlanId = it.basePlanId - - val product = CBProduct( - productId, - productTitle, - basePlanId, - price, - productDetail, - offerToken, - false, - productType - ) + val product = subscriptionCbProduct(productDetail, it) cbProductDetails.add(product) } val oneTimePurchaseOfferDetails = productDetail.oneTimePurchaseOfferDetails oneTimePurchaseOfferDetails?.let { - val price = it.formattedPrice - val product = CBProduct( - productId, - productTitle, - null, - price, - productDetail, - null, - false, - productType - ) + val product = oneTimeCbProduct(productDetail, it) cbProductDetails.add(product) } } @@ -162,6 +135,49 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } + private fun oneTimeCbProduct( + productDetail: ProductDetails, + it: ProductDetails.OneTimePurchaseOfferDetails + ): CBProduct { + val productId = productDetail.productId + val productType = ProductType.getProductType(productDetail.productType) + val productTitle = productDetail.title + val price = it.formattedPrice + return CBProduct( + productId, + productTitle, + null, + price, + productDetail, + null, + false, + productType + ) + } + + private fun subscriptionCbProduct( + productDetail: ProductDetails, + it: ProductDetails.SubscriptionOfferDetails + ): CBProduct { + val productId = productDetail.productId + val productType = ProductType.getProductType(productDetail.productType) + val productTitle = productDetail.title + val price = it.pricingPhases?.pricingPhaseList?.first()?.formattedPrice ?: "0" + val offerToken = it.offerToken + val basePlanId = it.basePlanId + + return CBProduct( + productId, + productTitle, + basePlanId, + price, + productDetail, + offerToken, + false, + productType + ) + } + internal fun purchase( product: CBProduct, purchaseCallBack: CBCallback.PurchaseCallback @@ -679,7 +695,6 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } ?: run { completionCallback.onError(itemNotOwnedException()) } - } else completionCallback.onError( connectionError diff --git a/chargebee/src/test/java/com/android/billingclient/api/StubProductDetails.kt b/chargebee/src/test/java/com/android/billingclient/api/StubProductDetails.kt new file mode 100644 index 0000000..9bff30c --- /dev/null +++ b/chargebee/src/test/java/com/android/billingclient/api/StubProductDetails.kt @@ -0,0 +1,38 @@ +package com.android.billingclient.api + +import kotlin.reflect.KClass + +fun KClass.create(): ProductDetails { + val productDetails = ProductDetails("{\n" + + "\t\"productId\": \"gold\",\n" + + "\t\"type\": \"subs\",\n" + + "\t\"title\": \"Gold Plan (com.chargebee.newsample (unreviewed))\",\n" + + "\t\"name\": \"Gold Plan\",\n" + + "\t\"localizedIn\": [\"en-US\"],\n" + + "\t\"skuDetailsToken\": \"AEuhp4Ln65Xw7Do9yIO-o4XIj7rAvn_FD90WWajD79kzt0GiNuNm2ACU15T3q56qZTs=\",\n" + + "\t\"subscriptionOfferDetails\": [{\n" + + "\t\t\"offerIdToken\": \"AUj\\/YhiCrZ\\/NvaFihCC8rjAWfXEsLtf\\/qPgutEo1M04GIc8psPY06GcWBpun6qf\\/NhMXcQe3KmD+rbgud2XiLO3ptF41\\/HWcHR7YfYcU7brJ6mM=\",\n" + + "\t\t\"basePlanId\": \"weekly\",\n" + + "\t\t\"pricingPhases\": [{\n" + + "\t\t\t\"priceAmountMicros\": 20000000,\n" + + "\t\t\t\"priceCurrencyCode\": \"INR\",\n" + + "\t\t\t\"formattedPrice\": \"₹20.00\",\n" + + "\t\t\t\"billingPeriod\": \"P1W\",\n" + + "\t\t\t\"recurrenceMode\": 1\n" + + "\t\t}],\n" + + "\t\t\"offerTags\": []\n" + + "\t}, {\n" + + "\t\t\"offerIdToken\": \"AUj\\/YhgOwTW\\/BAGR2Po8uAsNJc6G+Z5xSDRBnDU7VJ5GN21yhMvuUjUMFDNCwEu+GtDaN2CzYoLqu7wHu\\/T+37S1KlyLFi0tfSAZcJE5MisuY+hKUuRJ\",\n" + + "\t\t\"basePlanId\": \"monthly\",\n" + + "\t\t\"pricingPhases\": [{\n" + + "\t\t\t\"priceAmountMicros\": 40000000,\n" + + "\t\t\t\"priceCurrencyCode\": \"INR\",\n" + + "\t\t\t\"formattedPrice\": \"₹40.00\",\n" + + "\t\t\t\"billingPeriod\": \"P1M\",\n" + + "\t\t\t\"recurrenceMode\": 1\n" + + "\t\t}],\n" + + "\t\t\"offerTags\": []\n" + + "\t}]\n" + + "}") + return productDetails +} \ 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 1511cbe..5befe09 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -61,8 +61,9 @@ class BillingClientManagerTest { 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) + private val productDetails = ProductDetails::class.create() + private val otpProducts = CBProduct("test.consumable","Example product","basePlanId", "100.0", productDetails,"offerToken", true, productType = ProductType.INAPP) + private val subProducts = CBProduct("chargebee.premium.android","Premium Plan","basePlanId", "", productDetails,"offerToken", true, ProductType.SUBS) @Before fun setUp() { 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..38a6c60 100644 --- a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt @@ -4,8 +4,6 @@ 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.* @@ -21,6 +19,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.junit.MockitoJUnitRunner @@ -72,6 +71,9 @@ class RestorePurchaseTest { CBRestorePurchaseManager.fetchStoreSubscriptionStatus( purchaseTransaction, + purchaseTransaction, + purchaseTransaction, + arrayListOf(), completionCallback = object : RestorePurchaseCallback { override fun onSuccess(result: List) { lock.countDown() @@ -109,7 +111,7 @@ class RestorePurchaseTest { Matchers.instanceOf(CBException::class.java) ) Mockito.verify(CBRestorePurchaseManager, Mockito.times(1)) - .getRestorePurchases(purchaseTransaction) + .getRestorePurchases(any(), any(), any(), any()) }) } lock.await() From 4cf01e0983f0d59c3991affb84f2adc0eae38e10 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Tue, 17 Oct 2023 17:19:11 +0530 Subject: [PATCH 61/84] updates example app --- app/build.gradle | 9 ++++----- app/src/main/AndroidManifest.xml | 13 +++++++------ .../main/java/com/chargebee/example/MainActivity.kt | 1 - .../example/adapter/ProductListAdapter.java | 2 +- .../chargebee/example/billing/BillingActivity.java | 7 +++++++ .../com/chargebee/example/items/ItemActivity.kt | 2 -- .../com/chargebee/example/items/ItemsActivity.kt | 3 --- .../chargebee/example/plan/PlanInJavaActivity.java | 4 ---- .../com/chargebee/example/plan/PlansActivity.kt | 3 --- .../example/subscription/SubscriptionActivity.kt | 3 +-- .../com/chargebee/example/token/TokenViewModel.kt | 1 - .../com/chargebee/example/token/TokenizeActivity.kt | 1 - build.gradle | 4 ++-- chargebee/build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 15 files changed, 25 insertions(+), 34 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d1613a9..887970e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,10 +1,9 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 30 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -12,8 +11,8 @@ android { defaultConfig { applicationId "com.chargebee.example" minSdkVersion 21 - targetSdkVersion 30 - versionCode 3 + targetSdkVersion 33 + versionCode 6 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -41,7 +40,7 @@ dependencies { implementation 'com.google.android.material:material:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation 'com.google.code.gson:gson:2.8.8' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89f51ce..0ff6c0a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,12 +17,13 @@ - - - - - - + + + + + + diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index b158d4d..5520c6c 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -16,7 +16,6 @@ import androidx.recyclerview.widget.RecyclerView import com.chargebee.android.Chargebee import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBPurchase -import com.chargebee.android.models.CBRestoreSubscription import com.chargebee.android.exceptions.CBException import com.chargebee.android.models.CBProduct import com.chargebee.example.adapter.ListItemsAdapter diff --git a/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java b/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java index 4bba7c2..05d06f4 100644 --- a/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java +++ b/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java @@ -32,7 +32,7 @@ public ProductListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int vi @Override public void onBindViewHolder(ProductListAdapter.ViewHolder holder, int position) { CBProduct products = mProductsList.get(position); - holder.mTextViewTitle.setText(products.getProductId()); + holder.mTextViewTitle.setText(products.getProductId() + products.getProductBasePlanId()); holder.mTextViewPrice.setText(products.getProductPrice()); if (products.getSubStatus()) { holder.mTextViewSubscribe.setText(R.string.status_subscribed); 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 753151b..fba9c6d 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -1,18 +1,25 @@ package com.chargebee.example.billing; import android.app.Dialog; +import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; + +import androidx.annotation.NonNull; 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.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.example.BaseActivity; import com.chargebee.example.R; diff --git a/app/src/main/java/com/chargebee/example/items/ItemActivity.kt b/app/src/main/java/com/chargebee/example/items/ItemActivity.kt index c015368..afb345f 100644 --- a/app/src/main/java/com/chargebee/example/items/ItemActivity.kt +++ b/app/src/main/java/com/chargebee/example/items/ItemActivity.kt @@ -6,10 +6,8 @@ import android.widget.Button import android.widget.EditText import android.widget.TextView import androidx.lifecycle.Observer -import com.chargebee.android.ErrorDetail import com.chargebee.example.BaseActivity import com.chargebee.example.R -import com.google.gson.Gson class ItemActivity: BaseActivity() { diff --git a/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt b/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt index 921588e..9f1eb21 100644 --- a/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt +++ b/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt @@ -1,7 +1,6 @@ package com.chargebee.example.items import android.os.Bundle -import android.util.Log import android.view.View import android.widget.TextView import androidx.lifecycle.Observer @@ -9,11 +8,9 @@ import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.chargebee.android.Chargebee -import com.chargebee.android.ErrorDetail import com.chargebee.example.BaseActivity import com.chargebee.example.R import com.chargebee.example.adapter.ItemsAdapter -import com.google.gson.Gson class ItemsActivity : BaseActivity(), ItemsAdapter.ItemClickListener { diff --git a/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java b/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java index c53cefe..a211878 100644 --- a/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java +++ b/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java @@ -6,12 +6,8 @@ import android.widget.EditText; import android.widget.TextView; -import com.chargebee.android.ErrorDetail; -import com.chargebee.android.models.Plan; import com.chargebee.example.BaseActivity; import com.chargebee.example.R; -import com.google.gson.Gson; -import com.google.gson.JsonObject; public class PlanInJavaActivity extends BaseActivity { diff --git a/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt b/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt index 4d020bb..7e6890c 100644 --- a/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt +++ b/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt @@ -8,12 +8,9 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.chargebee.android.Chargebee -import com.chargebee.android.ErrorDetail import com.chargebee.example.BaseActivity import com.chargebee.example.R import com.chargebee.example.adapter.ItemsAdapter -import com.google.gson.Gson class PlansActivity : BaseActivity(), ItemsAdapter.ItemClickListener { diff --git a/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt b/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt index a8bf45f..f9b1dbb 100644 --- a/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt +++ b/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt @@ -3,7 +3,6 @@ package com.chargebee.example.subscription import android.os.Bundle import android.util.Log import android.widget.Button -import com.chargebee.android.Chargebee import com.chargebee.example.BaseActivity import com.chargebee.example.R import com.chargebee.example.billing.BillingViewModel @@ -39,7 +38,7 @@ class SubscriptionActivity : BaseActivity() { Log.i(javaClass.simpleName, "Subscriptions by using queryParams: $it") if(it?.size!! >0) { val subscriptionStatus = - it?.get(0).cb_subscription.status + "\nPlan Price : " + it?.get(0)?.cb_subscription?.plan_amount; + (it?.get(0)?.cb_subscription?.status ?: "") + "\nPlan Price : " + it?.get(0)?.cb_subscription?.plan_amount; alertSuccess(subscriptionStatus) }else{ alertSuccess("Subscriptions not found in Chargebee System") diff --git a/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt b/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt index 3847ac1..525f918 100644 --- a/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt +++ b/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.chargebee.android.Chargebee -import com.chargebee.android.models.Token import com.chargebee.android.exceptions.InvalidRequestException import com.chargebee.android.exceptions.OperationFailedException import com.chargebee.android.exceptions.PaymentException diff --git a/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt b/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt index 0e6a4b1..033de23 100644 --- a/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt +++ b/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt @@ -5,7 +5,6 @@ import android.util.Log import android.widget.Button import android.widget.EditText import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import com.chargebee.android.models.Card import com.chargebee.android.models.PaymentDetail diff --git a/build.gradle b/build.gradle index d866a33..129bae1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.3.72" + ext.kotlin_version = "1.8.20" ext.assertj_version = '3.16.1' repositories { google() jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.0.0" + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/chargebee/build.gradle b/chargebee/build.gradle index d216def..2d46fd0 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' // Google play billing library implementation 'com.android.billingclient:billing-ktx:5.2.1' @@ -54,7 +54,7 @@ dependencies { testImplementation 'org.json:json:20140107' - testImplementation 'androidx.test:core:1.2.0' + testImplementation 'androidx.test:core:1.4.0' testImplementation 'androidx.test.ext:junit:1.1.1' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c9a224..2e6e589 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d7341f7db5ed18f4a80b914ed8ad58af01bab6dc Mon Sep 17 00:00:00 2001 From: haripriyan Date: Thu, 9 Nov 2023 16:16:41 +0530 Subject: [PATCH 62/84] Updates Billing Library version to 5.2.1 --- app/build.gradle | 1 - .../example/subscription/SubscriptionActivity.kt | 2 +- build.gradle | 4 ++-- chargebee/build.gradle | 15 ++++++++------- .../main/java/com/chargebee/android/Chargebee.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d1613a9..67e8723 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { diff --git a/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt b/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt index a8bf45f..c8f9c5c 100644 --- a/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt +++ b/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt @@ -39,7 +39,7 @@ class SubscriptionActivity : BaseActivity() { Log.i(javaClass.simpleName, "Subscriptions by using queryParams: $it") if(it?.size!! >0) { val subscriptionStatus = - it?.get(0).cb_subscription.status + "\nPlan Price : " + it?.get(0)?.cb_subscription?.plan_amount; + it?.get(0)?.cb_subscription?.status + "\nPlan Price : " + it?.get(0)?.cb_subscription?.plan_amount; alertSuccess(subscriptionStatus) }else{ alertSuccess("Subscriptions not found in Chargebee System") diff --git a/build.gradle b/build.gradle index d866a33..a2f6fcb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.3.72" + ext.kotlin_version = "1.8.20" ext.assertj_version = '3.16.1' repositories { google() jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.0.0" + classpath "com.android.tools.build:gradle:4.2.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 838961e..7b2d19a 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -1,15 +1,14 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 1 - versionName "1.1.0" + versionName "1.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -39,10 +38,10 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' // Google play billing library - implementation 'com.android.billingclient:billing-ktx:4.0.0' + implementation 'com.android.billingclient:billing-ktx:5.2.1' // AssertJ testImplementation "org.assertj:assertj-core:$assertj_version" @@ -53,7 +52,9 @@ dependencies { // Mockito testImplementation 'org.mockito:mockito-core:2.23.0' - testImplementation 'androidx.test:core:1.2.0' + testImplementation 'androidx.test:core:1.4.0' + + testImplementation 'org.json:json:20140107' testImplementation 'androidx.test.ext:junit:1.1.1' } diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 5ecf590..763ee1f 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = BuildConfig.VERSION_NAME + const val sdkVersion: String = "1.2.0" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c9a224..2e6e589 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From c5f2a5e3b2c9d86515bcee547f9cbca5cf6350a0 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Thu, 9 Nov 2023 17:55:59 +0530 Subject: [PATCH 63/84] Updates Readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fadd420..8a7905e 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Post-installation, initialization, and authentication with the Chargebee site, t ## Requirements The following requirements must be set up before installing Chargebee’s Android SDK. -* Android 5.0 (API level 21) and above -* [Android Gradle Plugin](https://developer.android.com/studio/releases/gradle-plugin) 4.0.0 +* Android Target API Level 31 and above +* [Android Gradle Plugin](https://developer.android.com/studio/releases/gradle-plugin) 4.2.2 * [Gradle](https://gradle.org/releases/) 6.1.1+ * [AndroidX](https://developer.android.com/jetpack/androidx/) * Java 8+ and Kotlin @@ -21,7 +21,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.0.25' +implementation 'com.chargebee:chargebee-android:1.2.0' ``` ## Example project From 59ad17655d9bec7a3f7b58be622837cfac653d89 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Thu, 23 Nov 2023 17:57:34 +0530 Subject: [PATCH 64/84] Refactors Product for BL5 --- app/build.gradle | 2 +- .../chargebee/example/ExampleApplication.kt | 2 +- .../example/adapter/ProductListAdapter.java | 26 ++- .../example/adapter/PurchaseProduct.java | 58 +++++ .../example/billing/BillingActivity.java | 50 +++-- .../example/billing/BillingViewModel.kt | 34 +-- .../billingservice/BillingClientManager.kt | 211 +++++++++--------- .../android/billingservice/CBPurchase.kt | 49 ++-- .../com/chargebee/android/models/Products.kt | 31 ++- .../android/models/PurchaseProductParams.kt | 6 + .../BillingClientManagerTest.kt | 4 +- 11 files changed, 268 insertions(+), 205 deletions(-) create mode 100644 app/src/main/java/com/chargebee/example/adapter/PurchaseProduct.java create mode 100644 chargebee/src/main/java/com/chargebee/android/models/PurchaseProductParams.kt diff --git a/app/build.gradle b/app/build.gradle index 887970e..0f20f06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { } defaultConfig { applicationId "com.chargebee.example" - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion 33 versionCode 6 versionName "1.0" diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 5e7a473..02336f1 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().productType == ProductType.SUBS) + if (productIDs.first().type == ProductType.SUBS) validateReceipt(mContext, productIDs.first()) else validateNonSubscriptionReceipt(mContext, productIDs.first()) diff --git a/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java b/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java index 05d06f4..95da11e 100644 --- a/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java +++ b/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java @@ -7,19 +7,26 @@ import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; +import com.chargebee.android.billingservice.ProductType; import com.chargebee.android.models.CBProduct; +import com.chargebee.android.models.PricingPhase; +import com.chargebee.android.models.SubscriptionOffer; import com.chargebee.example.R; + +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; public class ProductListAdapter extends RecyclerView.Adapter { - private List mProductsList; + private List purchaseProducts; private ProductListAdapter.ProductClickListener mClickListener; private Context mContext = null; + private PurchaseProduct selectedProduct = null; - public ProductListAdapter(Context context, List mProductsList, ProductClickListener mClickListener) { + public ProductListAdapter(Context context, List purchaseProducts, ProductClickListener mClickListener) { mContext = context; - this.mProductsList = mProductsList; + this.purchaseProducts = purchaseProducts; this.mClickListener = mClickListener; } @@ -31,10 +38,12 @@ public ProductListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int vi @Override public void onBindViewHolder(ProductListAdapter.ViewHolder holder, int position) { - CBProduct products = mProductsList.get(position); - holder.mTextViewTitle.setText(products.getProductId() + products.getProductBasePlanId()); - holder.mTextViewPrice.setText(products.getProductPrice()); - if (products.getSubStatus()) { + PurchaseProduct purchaseProduct = purchaseProducts.get(position); + holder.mTextViewTitle.setText(purchaseProduct.getProductId() + " "+ purchaseProduct.getBasePlanId()); + holder.mTextViewPrice.setText(purchaseProduct.getPrice()); + boolean isSubscriptionProductSelected = selectedProduct != null && selectedProduct.getCbProduct().getType().equals(ProductType.SUBS) && selectedProduct.getOfferToken().equals(purchaseProduct.getOfferToken()); + boolean isOtpProductSelected = selectedProduct != null && selectedProduct.getCbProduct().getType().equals(ProductType.INAPP) && selectedProduct.getProductId().equals(purchaseProduct.getProductId()); + if (isSubscriptionProductSelected || isOtpProductSelected) { holder.mTextViewSubscribe.setText(R.string.status_subscribed); holder.mTextViewSubscribe.setTextColor(mContext.getResources().getColor(R.color.success_green)); }else { @@ -46,7 +55,7 @@ public void onBindViewHolder(ProductListAdapter.ViewHolder holder, int position) @Override public int getItemCount() { - return mProductsList.size(); + return purchaseProducts.size(); } public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { @@ -63,6 +72,7 @@ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickL @Override public void onClick(View view) { if (mClickListener != null) { + selectedProduct = purchaseProducts.get(getAdapterPosition()); mClickListener.onProductClick(view, getAdapterPosition()); } } diff --git a/app/src/main/java/com/chargebee/example/adapter/PurchaseProduct.java b/app/src/main/java/com/chargebee/example/adapter/PurchaseProduct.java new file mode 100644 index 0000000..7d152b8 --- /dev/null +++ b/app/src/main/java/com/chargebee/example/adapter/PurchaseProduct.java @@ -0,0 +1,58 @@ +package com.chargebee.example.adapter; + +import com.chargebee.android.models.CBProduct; +import com.chargebee.android.models.PricingPhase; +import com.chargebee.android.models.SubscriptionOffer; + +public class PurchaseProduct { + private final String productId; + private final CBProduct cbProduct; + private final String basePlanId; + private final String offerId; + private final String offerToken; + private final String price; + + public PurchaseProduct(CBProduct cbProduct, SubscriptionOffer subscriptionOffer) { + this(cbProduct.getId(), cbProduct, subscriptionOffer.getBasePlanId(), subscriptionOffer.getOfferId(), + subscriptionOffer.getOfferToken(), subscriptionOffer.getPricingPhases().get(0).getFormattedPrice()); + } + + public PurchaseProduct(CBProduct cbProduct, PricingPhase oneTimePurchaseOffer) { + this(cbProduct.getId(), cbProduct, + null, null, + null, oneTimePurchaseOffer.getFormattedPrice()); + } + + public PurchaseProduct(String id, CBProduct cbProduct, String basePlanId, String offerId, String offerToken, String formattedPrice) { + this.productId = id; + this.cbProduct = cbProduct; + this.basePlanId = basePlanId; + this.offerId = offerId; + this.offerToken = offerToken; + this.price = formattedPrice; + } + + public String getProductId() { + return productId; + } + + public String getBasePlanId() { + return basePlanId; + } + + public String getOfferId() { + return offerId; + } + + public String getOfferToken() { + return offerToken; + } + + public String getPrice() { + return price; + } + + public CBProduct getCbProduct() { + return cbProduct; + } +} \ No newline at end of file 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 fba9c6d..4c2c843 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -1,38 +1,40 @@ package com.chargebee.example.billing; +import static com.chargebee.example.util.Constants.PRODUCTS_LIST_KEY; + import android.app.Dialog; -import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; -import androidx.annotation.NonNull; 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.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.models.PurchaseProductParams; import com.chargebee.android.network.CBCustomer; import com.chargebee.example.BaseActivity; import com.chargebee.example.R; import com.chargebee.example.adapter.ProductListAdapter; +import com.chargebee.example.adapter.PurchaseProduct; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; + import java.lang.reflect.Type; import java.util.ArrayList; -import static com.chargebee.example.util.Constants.PRODUCTS_LIST_KEY; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; public class BillingActivity extends BaseActivity implements ProductListAdapter.ProductClickListener, ProgressBarListener { - private ArrayList productList = null; + private List purchaseProducts = null; private ProductListAdapter productListAdapter = null; private LinearLayoutManager linearLayoutManager; private RecyclerView mItemsRecyclerView = null; @@ -55,10 +57,14 @@ protected void onCreate(Bundle savedInstanceState) { if(productDetails != null) { Gson gson = new Gson(); Type listType = new TypeToken>() {}.getType(); - productList = gson.fromJson(productDetails, listType); + List productList = gson.fromJson(productDetails, listType); + this.purchaseProducts = productList.stream() + .map(x -> toList(x)) + .flatMap(List::stream) + .collect(Collectors.toList()); } - productListAdapter = new ProductListAdapter(this,productList, this); + productListAdapter = new ProductListAdapter(this, purchaseProducts, this); linearLayoutManager = new LinearLayoutManager(this); mItemsRecyclerView.setLayoutManager(linearLayoutManager); mItemsRecyclerView.setItemAnimator(new DefaultItemAnimator()); @@ -158,7 +164,6 @@ private void getCustomerID() { } } else { purchaseProduct(customerId); - // purchaseProduct(); dialog.dismiss(); } }); @@ -174,26 +179,23 @@ private boolean checkProductTypeFiled(){ } private boolean isOneTimeProduct(){ - return productList.get(position).getProductType() == ProductType.INAPP; + return purchaseProducts.get(position).getCbProduct().getType() == ProductType.INAPP; } private void purchaseProduct(String customerId) { showProgressDialog(); - this.billingViewModel.purchaseProduct(this, productList.get(position), customerId); - } - - private void purchaseProduct() { - showProgressDialog(); - this.billingViewModel.purchaseProduct(this, productList.get(position), cbCustomer); + PurchaseProduct selectedPurchaseProduct = purchaseProducts.get(position); + PurchaseProductParams purchaseParams = new PurchaseProductParams(selectedPurchaseProduct.getCbProduct(), selectedPurchaseProduct.getOfferToken()); + this.billingViewModel.purchaseProduct(this, purchaseParams, cbCustomer); } private void purchaseNonSubscriptionProduct(OneTimeProductType productType) { showProgressDialog(); - this.billingViewModel.purchaseNonSubscriptionProduct(this, productList.get(position), cbCustomer, productType); + CBProduct selectedProduct = purchaseProducts.get(position).getCbProduct(); + this.billingViewModel.purchaseNonSubscriptionProduct(this, selectedProduct, cbCustomer, productType); } private void updateSubscribeStatus(){ - productList.get(position).setSubStatus(true); productListAdapter.notifyDataSetChanged(); } @@ -207,4 +209,12 @@ public void onShowProgressBar() { public void onHideProgressBar() { hideProgressDialog(); } + private List toList(CBProduct cbProduct) { + if(cbProduct.getType() == ProductType.SUBS) { + return cbProduct.getSubscriptionOffers().stream() + .map(x -> new PurchaseProduct(cbProduct, x)).collect(Collectors.toList()); + } else { + return Arrays.asList(new PurchaseProduct(cbProduct, cbProduct.getOneTimePurchaseOffer())); + } + } } 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 1c66c00..8aa081a 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -28,10 +28,10 @@ class BillingViewModel : ViewModel() { private lateinit var sharedPreference : SharedPreferences var restorePurchaseResult: MutableLiveData> = MutableLiveData() - fun purchaseProduct(context: Context,product: CBProduct, customer: CBCustomer) { + fun purchaseProduct(context: Context, purchaseProductParams: PurchaseProductParams, customer: CBCustomer) { // 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.purchaseProduct(product, customer, object : CBCallback.PurchaseCallback{ + CBPurchase.purchaseProduct(purchaseProductParams, customer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { Log.i(TAG, "Subscription ID: ${result.subscription_id}") Log.i(TAG, "Plan ID: ${result.plan_id}") @@ -41,30 +41,8 @@ class BillingViewModel : ViewModel() { try { // Handled server not responding and offline if (error.httpStatusCode!! in 500..599) { - storeInLocal(product.productId) - validateReceipt(context = context, product = product) - } else { - cbException.postValue(error) - } - } catch (exp: Exception) { - Log.i(TAG, "Exception :${exp.message}") - } - } - }) - } - fun purchaseProduct(context: Context, product: CBProduct, customerId: String) { - sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) - CBPurchase.purchaseProduct(product, customerId, object : CBCallback.PurchaseCallback{ - override fun onSuccess(result: ReceiptDetail, status:Boolean) { - Log.i(TAG, "Subscription ID: ${result.subscription_id}") - Log.i(TAG, "Plan ID: ${result.plan_id}") - productPurchaseResult.postValue(status) - } - override fun onError(error: CBException) { - try { - if (error.httpStatusCode!! in 500..599) { - storeInLocal(product.productId) - validateReceipt(context = context, product = product) + storeInLocal(purchaseProductParams.product.id) + validateReceipt(context = context, product = purchaseProductParams.product) } else { cbException.postValue(error) } @@ -107,7 +85,7 @@ class BillingViewModel : ViewModel() { } fun retrieveProductIdentifers(queryParam: Array){ - CBPurchase.retrieveProductIdentifers(queryParam) { + CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { Log.i(TAG, "List of Product Identifiers: $it") @@ -197,7 +175,7 @@ class BillingViewModel : ViewModel() { try { // Handled server not responding and offline if (error.httpStatusCode!! in 500..599) { - storeInLocal(product.productId) + storeInLocal(product.id) validateNonSubscriptionReceipt(context = context, product = product, productType = productType) } else { cbException.postValue(error) 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 f9c4107..b71a091 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -13,7 +13,10 @@ 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.models.PricingPhase +import com.chargebee.android.models.PurchaseProductParams import com.chargebee.android.models.PurchaseTransaction +import com.chargebee.android.models.SubscriptionOffer import com.chargebee.android.network.CBReceiptResponse import com.chargebee.android.restore.CBRestorePurchaseManager @@ -25,7 +28,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private val handler = Handler(Looper.getMainLooper()) private var purchaseCallBack: CBCallback.PurchaseCallback? = null private val TAG = javaClass.simpleName - lateinit var product: CBProduct + private lateinit var purchaseProductParams: PurchaseProductParams private lateinit var restorePurchaseCallBack: CBCallback.RestorePurchaseCallback private var oneTimePurchaseCallback: CBCallback.OneTimePurchaseCallback? = null @@ -96,18 +99,8 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { try { val cbProductDetails = arrayListOf() for (productDetail in productsDetail) { - - val subscriptionOfferDetails = productDetail.subscriptionOfferDetails - subscriptionOfferDetails?.forEach { - val product = subscriptionCbProduct(productDetail, it) - cbProductDetails.add(product) - } - - val oneTimePurchaseOfferDetails = productDetail.oneTimePurchaseOfferDetails - oneTimePurchaseOfferDetails?.let { - val product = oneTimeCbProduct(productDetail, it) - cbProductDetails.add(product) - } + val cbProduct = convertToCbProduct(productDetail) + cbProductDetails.add(cbProduct) } Log.i(TAG, "Product details :$cbProductDetails") response(cbProductDetails) @@ -134,58 +127,61 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { errorDetail(CBException(ErrorDetail(message = "${exp.message}"))) } } - - private fun oneTimeCbProduct( - productDetail: ProductDetails, - it: ProductDetails.OneTimePurchaseOfferDetails - ): CBProduct { - val productId = productDetail.productId - val productType = ProductType.getProductType(productDetail.productType) - val productTitle = productDetail.title - val price = it.formattedPrice + private fun convertToCbProduct(productDetail: ProductDetails): CBProduct { + val subscriptionOffers = subscriptionOffers(productDetail.subscriptionOfferDetails) + val oneTimePurchaseOffer = oneTimePurchaseOffer(productDetail.oneTimePurchaseOfferDetails) return CBProduct( - productId, - productTitle, - null, - price, - productDetail, - null, - false, - productType + productDetail.productId, + productDetail.title, + productDetail.description, + ProductType.getProductType(productDetail.productType), + subscriptionOffers, + oneTimePurchaseOffer ) } - private fun subscriptionCbProduct( - productDetail: ProductDetails, - it: ProductDetails.SubscriptionOfferDetails - ): CBProduct { - val productId = productDetail.productId - val productType = ProductType.getProductType(productDetail.productType) - val productTitle = productDetail.title - val price = it.pricingPhases?.pricingPhaseList?.first()?.formattedPrice ?: "0" - val offerToken = it.offerToken - val basePlanId = it.basePlanId + private fun oneTimePurchaseOffer(oneTimePurchaseOfferDetails: ProductDetails.OneTimePurchaseOfferDetails?): PricingPhase? { + return oneTimePurchaseOfferDetails?.let { + return PricingPhase(oneTimePurchaseOfferDetails.formattedPrice, + oneTimePurchaseOfferDetails.priceAmountMicros, + oneTimePurchaseOfferDetails.priceCurrencyCode) + } + } + + private fun subscriptionOffers(subscriptionOfferDetails: List?): List? { + return subscriptionOfferDetails?.let { it.map { i -> subscriptionOffer(i) } } + } - return CBProduct( - productId, - productTitle, - basePlanId, - price, - productDetail, - offerToken, - false, - productType + private fun subscriptionOffer(subscriptionOfferDetail: ProductDetails.SubscriptionOfferDetails): SubscriptionOffer { + val pricingPhases = pricingPhases(subscriptionOfferDetail.pricingPhases) + return SubscriptionOffer( + subscriptionOfferDetail.basePlanId, + subscriptionOfferDetail.offerId, + subscriptionOfferDetail.offerToken, + pricingPhases ) } + private fun pricingPhases(pricingPhases: ProductDetails.PricingPhases): List { + return pricingPhases.pricingPhaseList.map { + PricingPhase( + it.formattedPrice, + it.priceAmountMicros, + it.priceCurrencyCode, + it.billingPeriod, + it.billingCycleCount + ) + } + } + internal fun purchase( - product: CBProduct, + purchaseProductParams: PurchaseProductParams, purchaseCallBack: CBCallback.PurchaseCallback ) { this.purchaseCallBack = purchaseCallBack onConnected({ status -> if (status) { - purchase(product) + purchase(purchaseProductParams) } else purchaseCallBack.onError( connectionError @@ -197,47 +193,58 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } /* Purchase the product: Initiates the billing flow for an In-app-purchase */ - private fun purchase(product: CBProduct) { - this.product = product - val productDetails = product.productDetails - val offerToken = product.offerToken - - val productDetailsParamsList = - listOf( - offerToken?.let { - BillingFlowParams.ProductDetailsParams.newBuilder() - .setProductDetails(productDetails) - .setOfferToken(it) + private fun purchase(purchaseProductParams: PurchaseProductParams) { + this.purchaseProductParams = purchaseProductParams + val offerToken = purchaseProductParams.offerToken + + val queryProductDetails = arrayListOf(QueryProductDetailsParams.Product.newBuilder() + .setProductId(purchaseProductParams.product.id) + .setProductType(purchaseProductParams.product.type.value) + .build()) + + val productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(queryProductDetails).build() + + billingClient?.queryProductDetailsAsync( + productDetailsParams + ) { billingResult, productsDetail -> + if (billingResult.responseCode == OK && productsDetail != null) { + val productDetailsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productsDetail.first()) + offerToken?.let { productDetailsBuilder.setOfferToken(it) } + val productDetailsParamsList = + listOf(productDetailsBuilder.build()) + + val billingFlowParams = + BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList) .build() - } - ) - val billingFlowParams = - BillingFlowParams.newBuilder() - .setProductDetailsParamsList(productDetailsParamsList) - .build() - - billingClient?.launchBillingFlow(mContext as Activity, billingFlowParams) - .takeIf { billingResult -> - billingResult?.responseCode != OK - }?.let { billingResult -> - Log.e(TAG, "Failed to launch billing flow $billingResult") - val billingError = CBException( - ErrorDetail( - message = GPErrorCode.LaunchBillingFlowError.errorMsg, - httpStatusCode = billingResult.responseCode - ) - ) - if (ProductType.SUBS == product.productType) { - purchaseCallBack?.onError( - billingError - ) - } else { - oneTimePurchaseCallback?.onError( - billingError - ) - } + billingClient?.launchBillingFlow(mContext as Activity, billingFlowParams) + .takeIf { billingResult -> + billingResult?.responseCode != OK + }?.let { billingResult -> + Log.e(TAG, "Failed to launch billing flow $billingResult") + val billingError = CBException( + ErrorDetail( + message = GPErrorCode.LaunchBillingFlowError.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + if (ProductType.SUBS == purchaseProductParams.product.type) { + purchaseCallBack?.onError( + billingError + ) + } else { + oneTimePurchaseCallback?.onError( + billingError + ) + } + } + } else { + // TODO: Handle error } + } + } /** @@ -319,7 +326,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } else -> { - if (product.productType == ProductType.SUBS) + if (purchaseProductParams.product.type == ProductType.SUBS) purchaseCallBack?.onError( throwCBException(billingResult) ) @@ -333,10 +340,10 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { /* Acknowledge the Purchases */ private fun acknowledgePurchase(purchase: Purchase) { - when (product.productType) { + when (purchaseProductParams.product.type) { ProductType.SUBS -> { isAcknowledgedPurchase(purchase, { - validateReceipt(purchase.purchaseToken, product) + validateReceipt(purchase.purchaseToken, purchaseProductParams.product) }, { purchaseCallBack?.onError(it) }) @@ -347,7 +354,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { consumeAsyncPurchase(purchase.purchaseToken) } else { isAcknowledgedPurchase(purchase, { - validateNonSubscriptionReceipt(purchase.purchaseToken, product) + validateNonSubscriptionReceipt(purchase.purchaseToken, purchaseProductParams.product) }, { oneTimePurchaseCallback?.onError(it) }) @@ -399,7 +406,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { consumePurchase(token) { billingResult, purchaseToken -> when (billingResult.responseCode) { OK -> { - validateNonSubscriptionReceipt(purchaseToken, product) + validateNonSubscriptionReceipt(purchaseToken, purchaseProductParams.product) } else -> { @@ -609,7 +616,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { if (status) queryPurchaseHistory { purchaseHistoryList -> val purchaseTransaction = purchaseHistoryList.filter { - it.productId.first() == product.productId + it.productId.first() == product.id } val transaction = purchaseTransaction.firstOrNull() transaction?.let { @@ -633,12 +640,12 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) { this.oneTimePurchaseCallback = oneTimePurchaseCallback onConnected({ status -> - if (status) - purchase(product) - else - oneTimePurchaseCallback.onError( - connectionError - ) + if (status) { + val purchaseParams = PurchaseProductParams(product) + purchase(purchaseParams) + } else { + oneTimePurchaseCallback.onError(connectionError) + } }, { error -> oneTimePurchaseCallback.onError(error) }) @@ -687,7 +694,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { if (status) queryPurchaseHistory { purchaseHistoryList -> val purchaseTransaction = purchaseHistoryList.filter { - it.productId.first() == product.productId + it.productId.first() == product.id } val transaction = purchaseTransaction.firstOrNull() transaction?.let { 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 34e871f..e5c41f6 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -25,7 +25,7 @@ object CBPurchase { * Get the product ID's from chargebee system */ @JvmStatic - fun retrieveProductIdentifers( + fun retrieveProductIdentifiers( params: Array = arrayOf(), completion: (CBProductIDResult>) -> Unit ) { @@ -54,41 +54,24 @@ object CBPurchase { } /** - * Buy the product with/without customer id - * @param [product] The product that wish to purchase - * @param [callback] listener will be called when product purchase completes. - */ - @Deprecated( - message = "This will be removed in upcoming release, Please use API fun - purchaseProduct(product: CBProduct, customer : CBCustomer? = null, callback)", - level = DeprecationLevel.WARNING - ) - @JvmStatic - fun purchaseProduct( - product: CBProduct, customerID: String, - callback: CBCallback.PurchaseCallback - ) { - customer = CBCustomer(customerID, "", "", "") - purchaseProduct(product, callback) - } - - /** - * Buy the product with/without customer data - * @param [product] The product that wish to purchase + * Buy Subscription product with/without customer data + * @param [purchaseParams] The purchase parameters of the product to be purchased. + * @param [customer] Optional. Customer Object. * @param [callback] listener will be called when product purchase completes. */ @JvmStatic fun purchaseProduct( - product: CBProduct, customer: CBCustomer? = null, + purchaseProductParams: PurchaseProductParams, customer: CBCustomer? = null, callback: CBCallback.PurchaseCallback ) { this.customer = customer - purchaseProduct(product, callback) + purchaseProduct(purchaseProductParams, callback) } - private fun purchaseProduct(product: CBProduct, callback: CBCallback.PurchaseCallback) { + private fun purchaseProduct(purchaseProductParams: PurchaseProductParams, callback: CBCallback.PurchaseCallback) { isSDKKeyValid({ - log(customer, product) - billingClientManager?.purchase(product, callback) + log(customer, purchaseProductParams.product.id) + billingClientManager?.purchase(purchaseProductParams, callback) }, { callback.onError(it) }) @@ -111,7 +94,7 @@ object CBPurchase { this.productType = productType isSDKKeyValid({ - log(CBPurchase.customer, product, productType) + log(CBPurchase.customer, product.id, productType) billingClientManager?.purchaseNonSubscriptionProduct(product, callback) }, { callback.onError(it) @@ -195,7 +178,7 @@ object CBPurchase { completion: (ChargebeeResult) -> Unit ) { try { - validateReceipt(purchaseToken, product.productId, completion) + validateReceipt(purchaseToken, product.id, completion) } catch (exp: Exception) { Log.e(javaClass.simpleName, "Exception in validateReceipt() :" + exp.message) ChargebeeResult.Error( @@ -257,7 +240,7 @@ object CBPurchase { product: CBProduct, completion: (ChargebeeResult) -> Unit ) { - validateNonSubscriptionReceipt(purchaseToken, product.productId, completion) + validateNonSubscriptionReceipt(purchaseToken, product.id, completion) } internal fun validateNonSubscriptionReceipt( @@ -384,8 +367,8 @@ object CBPurchase { return billingClientManager as BillingClientManager } - private fun log(customer: CBCustomer?, product: CBProduct, productType: OneTimeProductType? = null) { - val additionalInfo = additionalInfo(customer, product, productType) + private fun log(customer: CBCustomer?, productId: String, productType: OneTimeProductType? = null) { + val additionalInfo = additionalInfo(customer, productId, productType) val logger = CBLogger( name = "buy", action = "before_purchase_command", @@ -393,8 +376,8 @@ object CBPurchase { ) ResultHandler.safeExecute { logger.info() } } - private fun additionalInfo(customer: CBCustomer?, product: CBProduct, productType: OneTimeProductType? = null): Map { - val map = mutableMapOf("product" to product.productId) + private fun additionalInfo(customer: CBCustomer?, productId: String, productType: OneTimeProductType? = null): Map { + val map = mutableMapOf("product" to productId) customer?.let { map["customerId"] = (it.id ?: "") } productType?.let { map["productType"] = it.toString() } return map 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 ad9c608..a915bf6 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/Products.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/Products.kt @@ -1,15 +1,26 @@ package com.chargebee.android.models -import com.android.billingclient.api.ProductDetails import com.chargebee.android.billingservice.ProductType data class CBProduct( - val productId: String, - val productTitle: String, - val productBasePlanId: String?, - val productPrice: String, - var productDetails: ProductDetails, - var offerToken: String?, - var subStatus: Boolean, - var productType: ProductType -) \ No newline at end of file + val id: String, + val title: String, + val description: String, + val type: ProductType, + val subscriptionOffers: List?, + val oneTimePurchaseOffer: PricingPhase?, +) + +data class SubscriptionOffer( + val basePlanId: String, + val offerId: String?, + val offerToken: String, + val pricingPhases: List +) +data class PricingPhase( + val formattedPrice: String, + val amountInMicros: Long, + val currencyCode: String, + val billingPeriod: String? = null, + val billingCycleCount: Int? = null +) diff --git a/chargebee/src/main/java/com/chargebee/android/models/PurchaseProductParams.kt b/chargebee/src/main/java/com/chargebee/android/models/PurchaseProductParams.kt new file mode 100644 index 0000000..1d4dd71 --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/models/PurchaseProductParams.kt @@ -0,0 +1,6 @@ +package com.chargebee.android.models + +data class PurchaseProductParams( + val product: CBProduct, + val offerToken: String? = null +) \ 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 5befe09..4da7f86 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -151,7 +151,7 @@ class BillingClientManagerTest { val IDs = java.util.ArrayList() IDs.add("") CoroutineScope(Dispatchers.IO).launch { - Mockito.`when`(CBPurchase.retrieveProductIdentifers(queryParam) { + Mockito.`when`(CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { assertThat(it,instanceOf(CBProductIDResult::class.java)) @@ -235,7 +235,7 @@ class BillingClientManagerTest { val productsIds = java.util.ArrayList() productsIds.add("") CoroutineScope(Dispatchers.IO).launch { - Mockito.`when`(CBPurchase.retrieveProductIdentifers(queryParam) { + Mockito.`when`(CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { assertThat(it,instanceOf(CBProductIDResult::class.java)) From 09c6ddfba9a72930c2de40e84b3b72f3c2e71505 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Thu, 23 Nov 2023 23:58:52 +0530 Subject: [PATCH 65/84] Refactors example app --- .../chargebee/example/ExampleApplication.kt | 13 ++++--- .../com/chargebee/example/MainActivity.kt | 15 ++++++-- .../java/com/chargebee/android/Chargebee.kt | 35 ------------------- 3 files changed, 19 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 02336f1..0dae9b5 100644 --- a/app/src/main/java/com/chargebee/example/ExampleApplication.kt +++ b/app/src/main/java/com/chargebee/example/ExampleApplication.kt @@ -18,7 +18,7 @@ import com.chargebee.example.util.NetworkUtil class ExampleApplication : Application(), NetworkUtil.NetworkListener { private lateinit var networkUtil: NetworkUtil - private lateinit var sharedPreference: SharedPreferences + private var sharedPreference: SharedPreferences? = null lateinit var mContext: Context private val customer = CBCustomer( id = "sync_receipt_android", @@ -36,8 +36,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { } override fun onNetworkConnectionAvailable() { - Chargebee.configure(site = "", publishableApiKey= "",sdkKey= "", packageName = this.packageName) - val productId = sharedPreference.getString("productId", "") + val productId = sharedPreference?.getString("productId", "") if (productId?.isNotEmpty() == true) { val productList = ArrayList() productList.add(productId) @@ -76,8 +75,8 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { completionCallback = object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { // Clear the local cache once receipt validation success - val editor = sharedPreference.edit() - editor.clear().apply() + val editor = sharedPreference?.edit() + editor?.clear()?.apply() Log.i(javaClass.simpleName, "Subscription ID: ${result.subscription_id}") Log.i(javaClass.simpleName, "Plan ID: ${result.plan_id}") Log.i(javaClass.simpleName, "Customer ID: ${result.customer_id}") @@ -99,8 +98,8 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { 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() + 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}") diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index 5520c6c..98750f4 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -17,6 +17,7 @@ import com.chargebee.android.Chargebee import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBPurchase import com.chargebee.android.exceptions.CBException +import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.CBProduct import com.chargebee.example.adapter.ListItemsAdapter import com.chargebee.example.addon.AddonActivity @@ -175,8 +176,18 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { siteNameEditText.text.toString(), apiKeyEditText.text.toString(), true, - sdkKeyEditText.text.toString(), this.packageName - ) + sdkKeyEditText.text.toString(), + this.packageName + ) { + when (it) { + is ChargebeeResult.Success -> { + Log.i(javaClass.simpleName, "Configured") + } + is ChargebeeResult.Error -> { + Log.e(javaClass.simpleName, " Failed") + } + } + } } builder.show() } diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 785de24..d75c6e4 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -42,41 +42,6 @@ object Chargebee { private const val SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions?sku=%s&package=%s" - /* Configure the app details with chargebee system */ - fun configure( - site: String, - publishableApiKey: String, - allowErrorLogging: Boolean = true, - sdkKey: String = "", - packageName: String = "" - ) { - this.applicationId = packageName - this.publishableApiKey = publishableApiKey - this.site = site - this.encodedApiKey = Credentials.basic(publishableApiKey, "") - this.baseUrl = "https://${site}.chargebee.com/api/" - this.allowErrorLogging = allowErrorLogging - this.sdkKey = sdkKey - val auth = Auth(sdkKey, applicationId, appName, channel) - - CBAuthentication.authenticate(auth) { - when (it) { - is ChargebeeResult.Success -> { - Log.i(javaClass.simpleName, "Environment Setup Completed") - Log.i(javaClass.simpleName, " Response :${it.data}") - val response = it.data as CBAuthResponse - this.version = response.in_app_detail.product_catalog_version - this.applicationId = response.in_app_detail.app_id - this.appName = response.in_app_detail.app_name - } - is ChargebeeResult.Error -> { - Log.i(javaClass.simpleName, "Exception from server :${it.exp.message}") - this.version = CatalogVersion.Unknown.value - } - } - } - } - /* Configure the app details with chargebee system */ fun configure( site: String, From 4da4427d1a8c28c8b8cd12ace58936e34ac8e6d5 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Fri, 24 Nov 2023 00:00:49 +0530 Subject: [PATCH 66/84] Updates version --- chargebee/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 2d46fd0..ffe3cde 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 1 - versionName "2.0.0" + versionName "2.0.0-beta-1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From 0d6fe7daf181f68d2200e9fc61b11c79699e41c3 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Fri, 24 Nov 2023 16:32:45 +0530 Subject: [PATCH 67/84] Fixes unit tests --- app/src/main/res/layout/activity_main.xml | 2 +- .../java/com/chargebee/android/Chargebee.kt | 35 +++++++++++++++++++ .../billingservice/BillingClientManager.kt | 8 ++++- .../BillingClientManagerTest.kt | 33 ++++++++++------- .../android/fixtures/ProductFixtures.kt | 15 ++++++++ .../android/resources/ItemResourceTest.kt | 4 ++- 6 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 chargebee/src/test/java/com/chargebee/android/fixtures/ProductFixtures.kt diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 43cdfa6..931cca5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -22,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" - android:text="Chargebee Examples" + android:text="Chargebee Examples - V1.0" android:textSize="24sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index d75c6e4..785de24 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -42,6 +42,41 @@ object Chargebee { private const val SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions?sku=%s&package=%s" + /* Configure the app details with chargebee system */ + fun configure( + site: String, + publishableApiKey: String, + allowErrorLogging: Boolean = true, + sdkKey: String = "", + packageName: String = "" + ) { + this.applicationId = packageName + this.publishableApiKey = publishableApiKey + this.site = site + this.encodedApiKey = Credentials.basic(publishableApiKey, "") + this.baseUrl = "https://${site}.chargebee.com/api/" + this.allowErrorLogging = allowErrorLogging + this.sdkKey = sdkKey + val auth = Auth(sdkKey, applicationId, appName, channel) + + CBAuthentication.authenticate(auth) { + when (it) { + is ChargebeeResult.Success -> { + Log.i(javaClass.simpleName, "Environment Setup Completed") + Log.i(javaClass.simpleName, " Response :${it.data}") + val response = it.data as CBAuthResponse + this.version = response.in_app_detail.product_catalog_version + this.applicationId = response.in_app_detail.app_id + this.appName = response.in_app_detail.app_name + } + is ChargebeeResult.Error -> { + Log.i(javaClass.simpleName, "Exception from server :${it.exp.message}") + this.version = CatalogVersion.Unknown.value + } + } + } + } + /* Configure the app details with chargebee system */ fun configure( site: String, 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 b71a091..a59f2dd 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -241,7 +241,13 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } } else { - // TODO: Handle error + Log.e(TAG, "Failed to fetch product :" + billingResult.responseCode) + if (ProductType.SUBS == purchaseProductParams.product.type) { + purchaseCallBack?.onError(throwCBException(billingResult)) + } else { + oneTimePurchaseCallback?.onError(throwCBException(billingResult)) + } + } } 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 4da7f86..700f81b 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -10,9 +10,12 @@ 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.fixtures.otpProducts +import com.chargebee.android.fixtures.subProducts import com.chargebee.android.models.CBNonSubscriptionResponse import com.chargebee.android.models.CBProduct import com.chargebee.android.models.NonSubscription +import com.chargebee.android.models.PurchaseProductParams import com.chargebee.android.network.* import com.chargebee.android.resources.CatalogVersion import com.chargebee.android.resources.ReceiptResource @@ -62,8 +65,7 @@ class BillingClientManagerTest { private var callBackOneTimePurchase: CBCallback.OneTimePurchaseCallback? = null private val nonSubscriptionDetail = NonSubscription("invoiceId", "customerId", "chargeId") private val productDetails = ProductDetails::class.create() - private val otpProducts = CBProduct("test.consumable","Example product","basePlanId", "100.0", productDetails,"offerToken", true, productType = ProductType.INAPP) - private val subProducts = CBProduct("chargebee.premium.android","Premium Plan","basePlanId", "", productDetails,"offerToken", true, ProductType.SUBS) + @Before fun setUp() { @@ -256,8 +258,9 @@ class BillingClientManagerTest { val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,"", + purchaseProductParams,null, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -271,10 +274,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(subProducts, it) + billingClientManager?.purchase(purchaseProductParams, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(purchaseProductParams,purchaseCallBack = it) } } lock.await() @@ -285,8 +288,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,"", + purchaseProductParams,null, object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { @@ -364,8 +368,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,customer, + purchaseProductParams,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -379,10 +384,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(subProducts, it) + billingClientManager?.purchase(purchaseProductParams, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(purchaseProductParams,purchaseCallBack = it) } } lock.await() @@ -395,8 +400,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,customer, + purchaseProductParams,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -410,10 +416,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(subProducts, it) + billingClientManager?.purchase(purchaseProductParams, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(purchaseProductParams,purchaseCallBack = it) } } lock.await() @@ -424,8 +430,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,customer, + purchaseProductParams,customer, object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { diff --git a/chargebee/src/test/java/com/chargebee/android/fixtures/ProductFixtures.kt b/chargebee/src/test/java/com/chargebee/android/fixtures/ProductFixtures.kt new file mode 100644 index 0000000..f25002d --- /dev/null +++ b/chargebee/src/test/java/com/chargebee/android/fixtures/ProductFixtures.kt @@ -0,0 +1,15 @@ +package com.chargebee.android.fixtures + +import com.chargebee.android.billingservice.ProductType +import com.chargebee.android.models.CBProduct +import com.chargebee.android.models.PricingPhase +import com.chargebee.android.models.SubscriptionOffer + +val subsPricingPhase: PricingPhase = PricingPhase(formattedPrice = "1100.0 INR", amountInMicros = 1100000, currencyCode = "INR") +val subscriptionOffers: List = arrayListOf(SubscriptionOffer("basePlanId", "offerId", "offerToken", arrayListOf(subsPricingPhase))) +val oneTimePurchaseOffer: PricingPhase = PricingPhase(formattedPrice = "100.0 INR", amountInMicros = 100000, currencyCode = "INR") +val otpProducts = CBProduct("test.consumable","Example product", + "Description",ProductType.INAPP,null, oneTimePurchaseOffer) +val subProducts = CBProduct("chargebee.premium.android","Premium Plan", + "Description",ProductType.SUBS, subscriptionOffers, null) + diff --git a/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt b/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt index fff77f4..a987277 100644 --- a/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt @@ -32,7 +32,9 @@ class ItemResourceTest { site = "cb-imay-test", publishableApiKey = "test_EojsGoGFeHoc3VpGPQDOZGAxYy3d0FF3", sdkKey = "cb-j53yhbfmtfhfhkmhow3ramecom" - ) + ) { + + } } @After From 41025c14e8bf85c0b7db518388a6fb484079d779 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Fri, 24 Nov 2023 22:45:52 +0530 Subject: [PATCH 68/84] Updates docs --- README.md | 46 +++++++++++++------ .../com/chargebee/example/MainActivity.kt | 9 ++-- .../example/billing/BillingViewModel.kt | 2 +- .../android/billingservice/CBPurchase.kt | 2 +- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8a7905e..706cc2d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # Chargebee Android + +> #### Updates for Billing Library 5 +> ***Note**: If you want to simply update your app to use Google Billing Library 5, without having to make related changes, you can use the [1.x version](https://github.com/chargebee/chargebee-android/tree/1.x.x). You can upgrade to the latest version once you are ready. +> +> If you are getting started with the integration or ready to leverage Google Billing Library 5 changes, follow this [link](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html) for getting started. + This is Chargebee’s Android Software Development Kit (SDK). This SDK makes it efficient and comfortable to build a seamless subscription experience in your Android app. Post-installation, initialization, and authentication with the Chargebee site, this SDK will support the following process. @@ -21,7 +27,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.2.0' +implementation 'com.chargebee:chargebee-android:2.0.0-beta-1' ``` ## Example project @@ -55,9 +61,21 @@ To configure the Chargebee Android SDK for completing and managing In-App Purcha ```kotlin import com.chargebee.android.Chargebee -Chargebee.configure(site= "your-site", - publishableApiKey= "api_key", - sdkKey= "sdk_key",packageName = "packageName") +Chargebee.configure( + site = "your-site", + publishableApiKey = "api-key", + sdkKey = "sdk-key", + packageName = "your-package" +) { + when (it) { + is ChargebeeResult.Success -> { + // Success + } + is ChargebeeResult.Error -> { + // Error + } + } +} ``` ### Configuration for credit card using tokenization To configure SDK only for tokenizing credit card details, follow these steps. @@ -69,7 +87,7 @@ To configure SDK only for tokenizing credit card details, follow these steps. ```kotlin import com.chargebee.android.Chargebee -Chargebee.configure(site = "your-site", publishableApiKey = "api_key") +Chargebee.configure(site = "your-site", publishableApiKey = "api-key") ``` ## Integration @@ -86,7 +104,7 @@ The following section describes how to use the SDK to integrate In-App Purchase Every In-App Purchase subscription product that you configure in your Play Store account, can be configured in Chargebee as a Plan. Start by retrieving the Google IAP Product IDs from your Chargebee account. ```kotlin -CBPurchase.retrieveProductIdentifers(queryParam) { +CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { Log.i(TAG, "List of Product Identifiers: $it") @@ -106,29 +124,29 @@ The above function will determine your product catalog version in Chargebee and Retrieve the Google IAP Product using the following function. ```kotlin -CBPurchase.retrieveProducts(this, productIdList= "[Product ID's from Google Play Console]", - object : CBCallback.ListProductsCallback> { - override fun onSuccess(productDetails: ArrayList) { +CBPurchase.retrieveProducts(activity, productIdList= ["Product ID's from Google Play Console"], + object : CBCallback.ListProductsCallback> { + override fun onSuccess(productDetails: ArrayList) { Log.i(TAG, "List of Products: $productDetails") } override fun onError(error: CBException) { Log.e(TAG, "Error: ${error.message}") - // Handle error here } }) - ``` You can present any of the above products to your users for them to purchase. ### Buy or Subscribe Product -Pass the `CBProduct` and `CBCustomer` objects to the following function when the user chooses the product to purchase. +Pass the `PurchaseProductParams`, `CBCustomer` and `OfferToken` to the following function when the user chooses the product to purchase. `CBCustomer` - **Optional object**. Although this is an optional object, we recommend passing the necessary customer details, such as `customerId`, `firstName`, `lastName`, and `email` if it is available before the user subscribes to your App. This ensures that the customer details in your database match the customer details in Chargebee. If the `customerId` is not passed in the customer's details, then the value of `customerId` will be the same as the `SubscriptionId` created in Chargebee. **Note**: The `customer` parameter in the below code snippet is an instance of `CBCustomer` class that contains the details of the customer who wants to subscribe or buy the product. ```kotlin -CBPurchase.purchaseProduct(product=CBProduct, customer=CBCustomer, object : CBCallback.PurchaseCallback{ +val purchaseParams = PurchaseProductParams(selectedCBProduct, "selectedOfferToken") +val cbCustomer = CBCustomer("customerId","firstName","lastName","email") +CBPurchase.purchaseProduct(purchaseProductParams = purchaseProductParams, customer = cbCustomer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { Log.i(TAG, "$status") Log.i(TAG, "${result.subscription_id}") @@ -162,7 +180,7 @@ 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. +- `product`: An instance of `CBProduct` class, 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. diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index 98750f4..a16cc99 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -173,11 +173,10 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { ) && !TextUtils.isEmpty(sdkKeyEditText.text.toString()) ) Chargebee.configure( - siteNameEditText.text.toString(), - apiKeyEditText.text.toString(), - true, - sdkKeyEditText.text.toString(), - this.packageName + site = siteNameEditText.text.toString(), + publishableApiKey = apiKeyEditText.text.toString(), + sdkKey = sdkKeyEditText.text.toString(), + packageName = this.packageName ) { when (it) { is ChargebeeResult.Success -> { 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 8aa081a..1fdf2ee 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -31,7 +31,7 @@ class BillingViewModel : ViewModel() { fun purchaseProduct(context: Context, purchaseProductParams: PurchaseProductParams, customer: CBCustomer) { // 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.purchaseProduct(purchaseProductParams, customer, object : CBCallback.PurchaseCallback{ + CBPurchase.purchaseProduct(purchaseProductParams = purchaseProductParams, customer = customer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { Log.i(TAG, "Subscription ID: ${result.subscription_id}") Log.i(TAG, "Plan ID: ${result.plan_id}") 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 e5c41f6..079a2e0 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -55,7 +55,7 @@ object CBPurchase { /** * Buy Subscription product with/without customer data - * @param [purchaseParams] The purchase parameters of the product to be purchased. + * @param [purchaseProductParams] The purchase parameters of the product to be purchased. * @param [customer] Optional. Customer Object. * @param [callback] listener will be called when product purchase completes. */ From ed3513f9c9eb39ab1bcbb5a2fb2ed23fce02d5d8 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Fri, 24 Nov 2023 22:49:18 +0530 Subject: [PATCH 69/84] Updates version --- chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 785de24..a7995a6 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = "2.0.0" + const val sdkVersion: String = "2.0.0-beta-1" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" From 8ec2e4a421505908841d6d6b73e8957aa16a9184 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Mon, 27 Nov 2023 19:28:28 +0530 Subject: [PATCH 70/84] Updates readme --- README.md | 8 +++++--- .../main/java/com/chargebee/example/ExampleApplication.kt | 1 - .../com/chargebee/example/billing/BillingActivity.java | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 706cc2d..afd7ef2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # Chargebee Android > #### Updates for Billing Library 5 -> ***Note**: If you want to simply update your app to use Google Billing Library 5, without having to make related changes, you can use the [1.x version](https://github.com/chargebee/chargebee-android/tree/1.x.x). You can upgrade to the latest version once you are ready. -> -> If you are getting started with the integration or ready to leverage Google Billing Library 5 changes, follow this [link](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html) for getting started. +> ***Note**: +> - SDK Version 2.0: This version uses Google Billing Library 5.2.1 APIs to fetch product information from the Google Play Console and make purchases. If you’re integrating Chargebee’s SDK for the first time, then use this version, and if you’re migrating from the older version of SDK to this version, follow the migration steps in this [document](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html). +> - SDK Version 1.2.0: This version includes Billing Library 5.2.1 but still uses Billing Library 4.0 APIs to fetch product information from the Google Play Console and make purchases. This will enable you to list or update your Android app on the store without any warnings from Google and give you enough time to migrate to version 2.0. +> - SDK Version 1.1.0: This and less than this version of SDKs use billing library 4.0 APIs that are deprecated by Google. Therefore, it is highly recommended that you upgrade your app and integrate it with SDK version 1.2.0 and above. + This is Chargebee’s Android Software Development Kit (SDK). This SDK makes it efficient and comfortable to build a seamless subscription experience in your Android app. diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 0dae9b5..fc678ac 100644 --- a/app/src/main/java/com/chargebee/example/ExampleApplication.kt +++ b/app/src/main/java/com/chargebee/example/ExampleApplication.kt @@ -2,7 +2,6 @@ package com.chargebee.example import android.app.Application import android.content.Context -import com.chargebee.android.Chargebee import android.content.SharedPreferences import android.util.Log import com.chargebee.android.billingservice.CBCallback 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 4c2c843..231ffa4 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -163,7 +163,7 @@ private void getCustomerID() { dialog.dismiss(); } } else { - purchaseProduct(customerId); + purchaseProduct(); dialog.dismiss(); } }); @@ -182,7 +182,7 @@ private boolean isOneTimeProduct(){ return purchaseProducts.get(position).getCbProduct().getType() == ProductType.INAPP; } - private void purchaseProduct(String customerId) { + private void purchaseProduct() { showProgressDialog(); PurchaseProduct selectedPurchaseProduct = purchaseProducts.get(position); PurchaseProductParams purchaseParams = new PurchaseProductParams(selectedPurchaseProduct.getCbProduct(), selectedPurchaseProduct.getOfferToken()); From 015435bce20d8f0ad51b87dc84f75964ca76435a Mon Sep 17 00:00:00 2001 From: haripriyan Date: Mon, 27 Nov 2023 20:18:48 +0530 Subject: [PATCH 71/84] Updates readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index afd7ef2..a7012f6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > #### Updates for Billing Library 5 > ***Note**: > - SDK Version 2.0: This version uses Google Billing Library 5.2.1 APIs to fetch product information from the Google Play Console and make purchases. If you’re integrating Chargebee’s SDK for the first time, then use this version, and if you’re migrating from the older version of SDK to this version, follow the migration steps in this [document](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html). -> - SDK Version 1.2.0: This version includes Billing Library 5.2.1 but still uses Billing Library 4.0 APIs to fetch product information from the Google Play Console and make purchases. This will enable you to list or update your Android app on the store without any warnings from Google and give you enough time to migrate to version 2.0. +> - SDK Version 1.2.0: This [version](https://github.com/chargebee/chargebee-android/tree/1.x.x) includes Billing Library 5.2.1 but still uses Billing Library 4.0 APIs to fetch product information from the Google Play Console and make purchases. This will enable you to list or update your Android app on the store without any warnings from Google and give you enough time to migrate to version 2.0. > - SDK Version 1.1.0: This and less than this version of SDKs use billing library 4.0 APIs that are deprecated by Google. Therefore, it is highly recommended that you upgrade your app and integrate it with SDK version 1.2.0 and above. From c7725220ffa8d90c2b4549651226ae15f05e180c Mon Sep 17 00:00:00 2001 From: cb-haripriyan <67890824+cb-haripriyan@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:33:03 +0530 Subject: [PATCH 72/84] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7012f6..355cd85 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Chargebee Android +> [!NOTE] > #### Updates for Billing Library 5 -> ***Note**: > - SDK Version 2.0: This version uses Google Billing Library 5.2.1 APIs to fetch product information from the Google Play Console and make purchases. If you’re integrating Chargebee’s SDK for the first time, then use this version, and if you’re migrating from the older version of SDK to this version, follow the migration steps in this [document](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html). > - SDK Version 1.2.0: This [version](https://github.com/chargebee/chargebee-android/tree/1.x.x) includes Billing Library 5.2.1 but still uses Billing Library 4.0 APIs to fetch product information from the Google Play Console and make purchases. This will enable you to list or update your Android app on the store without any warnings from Google and give you enough time to migrate to version 2.0. > - SDK Version 1.1.0: This and less than this version of SDKs use billing library 4.0 APIs that are deprecated by Google. Therefore, it is highly recommended that you upgrade your app and integrate it with SDK version 1.2.0 and above. From 5e9058d2cab6289752eb38b7ae17fe6b89ea4135 Mon Sep 17 00:00:00 2001 From: haripriyan Date: Mon, 15 Apr 2024 20:05:10 +0530 Subject: [PATCH 73/84] Passes customer params when customer id is empty --- .../android/network/CBReceiptRequestBody.kt | 15 +++++++++++---- .../android/resources/ReceiptResource.kt | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) 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 c129f3b..d21945e 100644 --- a/chargebee/src/main/java/com/chargebee/android/network/CBReceiptRequestBody.kt +++ b/chargebee/src/main/java/com/chargebee/android/network/CBReceiptRequestBody.kt @@ -1,5 +1,6 @@ package com.chargebee.android.network +import android.text.TextUtils import com.chargebee.android.billingservice.OneTimeProductType internal class CBReceiptRequestBody( @@ -31,15 +32,18 @@ internal class CBReceiptRequestBody( } fun toCBReceiptReqCustomerBody(): Map { - return mapOf( + val params = mutableMapOf( "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 ) + if(!TextUtils.isEmpty(this.customer?.id)) { + params["customer[id]"] = this.customer?.id + } + return params } fun toMap(): Map { @@ -51,16 +55,19 @@ internal class CBReceiptRequestBody( } fun toCBNonSubscriptionReqCustomerBody(): Map { - return mapOf( + val params = mutableMapOf( "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 ) + if(!TextUtils.isEmpty(this.customer?.id)) { + params["customer[id]"] = this.customer?.id + } + return params } fun toMapNonSubscription(): Map { 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 0210364..4ae3ee6 100644 --- a/chargebee/src/main/java/com/chargebee/android/resources/ReceiptResource.kt +++ b/chargebee/src/main/java/com/chargebee/android/resources/ReceiptResource.kt @@ -13,7 +13,7 @@ internal class ReceiptResource : BaseResource(baseUrl = Chargebee.baseUrl){ internal suspend fun validateReceipt(params: Params): ChargebeeResult { val paramDetail = CBReceiptRequestBody.fromCBReceiptReqBody(params) - val dataMap = if (params.customer != null && !(TextUtils.isEmpty(params.customer.id))) { + val dataMap = if (params.customer != null) { paramDetail.toCBReceiptReqCustomerBody() } else{ paramDetail.toMap() @@ -29,7 +29,7 @@ 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))) { + val dataMap = if (params.customer != null) { paramDetail.toCBNonSubscriptionReqCustomerBody() } else{ paramDetail.toMapNonSubscription() From 085ade632fb5affc6f9189e6a37dad226e72a08b Mon Sep 17 00:00:00 2001 From: haripriyan Date: Mon, 15 Apr 2024 20:06:46 +0530 Subject: [PATCH 74/84] bumps version to 2.0.0-beta-2 --- README.md | 2 +- chargebee/build.gradle | 2 +- chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 355cd85..b1a3174 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:2.0.0-beta-1' +implementation 'com.chargebee:chargebee-android:2.0.0-beta-2' ``` ## Example project diff --git a/chargebee/build.gradle b/chargebee/build.gradle index ffe3cde..6e8e7dc 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 1 - versionName "2.0.0-beta-1" + versionName "2.0.0-beta-2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index a7995a6..708bfe3 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = "2.0.0-beta-1" + const val sdkVersion: String = "2.0.0-beta-2" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" From 1270deab8f4eb7eeea8c8b222bb7c534fa4a01d9 Mon Sep 17 00:00:00 2001 From: cb-palanim Date: Thu, 18 Apr 2024 19:18:49 +0530 Subject: [PATCH 75/84] Fix multiple callbacks - 2 https://github.com/chargebee/chargebee-android/pull/96/files Applying changes from 1.x.x version 2.x.x --- .../billingservice/BillingClientManager.kt | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 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 a59f2dd..b2c6d35 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -19,6 +19,7 @@ import com.chargebee.android.models.PurchaseTransaction import com.chargebee.android.models.SubscriptionOffer import com.chargebee.android.network.CBReceiptResponse import com.chargebee.android.restore.CBRestorePurchaseManager +import java.util.concurrent.ConcurrentLinkedQueue class BillingClientManager(context: Context) : PurchasesUpdatedListener { @@ -31,7 +32,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private lateinit var purchaseProductParams: PurchaseProductParams private lateinit var restorePurchaseCallBack: CBCallback.RestorePurchaseCallback private var oneTimePurchaseCallback: CBCallback.OneTimePurchaseCallback? = null - + private val requests = ConcurrentLinkedQueue Unit, (connectionError: CBException) -> Unit>>() init { this.mContext = context } @@ -580,35 +581,51 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private fun onConnected(status: (Boolean) -> Unit, connectionError: (CBException) -> Unit) { val billingClient = buildBillingClient(this) + requests.add(Pair(status, connectionError)) if (billingClient?.isReady == false) { - handler.postDelayed({ - billingClient.startConnection( - createBillingClientStateListener(status, connectionError) - ) - }, CONNECT_TIMER_START_MILLISECONDS) - } else status(true) + billingClient.startConnection( + createBillingClientStateListener() + ) + } else { + executeRequestsInQueue() + } + } + + @Synchronized + private fun executeRequestsInQueue() { + val head = requests.poll() + if (head != null) { + val successHandler = head.first + handler.post { + successHandler(true) + } + } } - private fun createBillingClientStateListener( - status: (Boolean) -> Unit, - connectionError: (CBException) -> Unit - ) = object : - BillingClientStateListener { + @Synchronized + private fun sendErrorToRequestsInQueue(exception: CBException) { + val head = requests.poll() + if(head != null) { + val exceptionHandler = head.second + handler.post { + exceptionHandler(exception) + } + } + } + + private fun createBillingClientStateListener() = object : BillingClientStateListener { override fun onBillingServiceDisconnected() { Log.i(javaClass.simpleName, "onBillingServiceDisconnected") - status(false) } override fun onBillingSetupFinished(billingResult: BillingResult) { when (billingResult.responseCode) { OK -> { Log.i(TAG, "Google Billing Setup Done!") - status(true) - } - - else -> { - connectionError(throwCBException(billingResult)) - } + executeRequestsInQueue() + } else -> { + sendErrorToRequestsInQueue(throwCBException(billingResult)) + } } } } From 81c6d80791b350606b98c3aaa856d038dea4b6d7 Mon Sep 17 00:00:00 2001 From: cb-palanim Date: Thu, 18 Apr 2024 19:20:25 +0530 Subject: [PATCH 76/84] Updating version numbers Updating version numbers --- README.md | 2 +- chargebee/build.gradle | 2 +- chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b1a3174..7c88ed1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:2.0.0-beta-2' +implementation 'com.chargebee:chargebee-android:2.0.0-beta-3' ``` ## Example project diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 6e8e7dc..cd03b91 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 1 - versionName "2.0.0-beta-2" + versionName "2.0.0-beta-3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 708bfe3..9518689 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = "2.0.0-beta-2" + const val sdkVersion: String = "2.0.0-beta-3" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" From 8d389b201212e9df4c23d0a2da2423a6244d2ed7 Mon Sep 17 00:00:00 2001 From: cb-palanim Date: Thu, 18 Jul 2024 20:07:09 +0530 Subject: [PATCH 77/84] Updating billing client version 5.2.1 to 6.2.1 --- chargebee/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chargebee/build.gradle b/chargebee/build.gradle index cd03b91..9e792a8 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -41,7 +41,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' // Google play billing library - implementation 'com.android.billingclient:billing-ktx:5.2.1' + implementation 'com.android.billingclient:billing-ktx:6.2.1' // AssertJ testImplementation "org.assertj:assertj-core:$assertj_version" From 3577cb43716627d10864bdd6e6224a2e4f1dec01 Mon Sep 17 00:00:00 2001 From: cb-palanim Date: Thu, 18 Jul 2024 20:14:05 +0530 Subject: [PATCH 78/84] Updating version Name --- chargebee/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 9e792a8..62c4ae4 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 1 - versionName "2.0.0-beta-3" + versionName "2.0.0-beta-4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From dfb427d96a71c8620f35ee33f11d9688f54c262d Mon Sep 17 00:00:00 2001 From: cb-palanim Date: Thu, 18 Jul 2024 20:22:34 +0530 Subject: [PATCH 79/84] Updating sdk version name --- chargebee/src/main/java/com/chargebee/android/Chargebee.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 9518689..8fa3dc2 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = "2.0.0-beta-3" + const val sdkVersion: String = "2.0.0-beta-4" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" From e8d03bb14ae8e8d88a5f49f423e03812482c4c8b Mon Sep 17 00:00:00 2001 From: cb-palanim Date: Thu, 18 Jul 2024 20:36:57 +0530 Subject: [PATCH 80/84] Update readme. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c88ed1..131e298 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ > [!NOTE] > #### Updates for Billing Library 5 -> - SDK Version 2.0: This version uses Google Billing Library 5.2.1 APIs to fetch product information from the Google Play Console and make purchases. If you’re integrating Chargebee’s SDK for the first time, then use this version, and if you’re migrating from the older version of SDK to this version, follow the migration steps in this [document](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html). -> - SDK Version 1.2.0: This [version](https://github.com/chargebee/chargebee-android/tree/1.x.x) includes Billing Library 5.2.1 but still uses Billing Library 4.0 APIs to fetch product information from the Google Play Console and make purchases. This will enable you to list or update your Android app on the store without any warnings from Google and give you enough time to migrate to version 2.0. +> - SDK Version 2.0: This version includes Google Billing Library 6.2.1 but still uses Google Billing Library 5.0 APIs to fetch product information from the Google Play Console and make purchases. If you’re integrating Chargebee’s SDK for the first time, then use this version, and if you’re migrating from the older version of SDK to this version, follow the migration steps in this [document](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html). +> - SDK Version 1.2.2: This [version](https://github.com/chargebee/chargebee-android/tree/1.x.x) includes Billing Library 6.2.1 but still uses Billing Library 4.0 APIs to fetch product information from the Google Play Console and make purchases. This will enable you to list or update your Android app on the store without any warnings from Google and give you enough time to migrate to version 2.0. > - SDK Version 1.1.0: This and less than this version of SDKs use billing library 4.0 APIs that are deprecated by Google. Therefore, it is highly recommended that you upgrade your app and integrate it with SDK version 1.2.0 and above. From 71d128527cbb75fb06fd118ac76c012169e93b21 Mon Sep 17 00:00:00 2001 From: cb-palanim Date: Thu, 18 Jul 2024 20:41:45 +0530 Subject: [PATCH 81/84] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 131e298..ba2bec0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:2.0.0-beta-3' +implementation 'com.chargebee:chargebee-android:2.0.0-beta-4' ``` ## Example project From a07f38902af2747b98a688e498e84d4bfe9ab491 Mon Sep 17 00:00:00 2001 From: Rohit Sharma Date: Thu, 27 Mar 2025 16:26:23 +0530 Subject: [PATCH 82/84] Secret scan workflow added for all the prs --- .github/workflows/secret.scan.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/workflows/secret.scan.yml diff --git a/.github/workflows/secret.scan.yml b/.github/workflows/secret.scan.yml new file mode 100644 index 0000000..bcdb1c0 --- /dev/null +++ b/.github/workflows/secret.scan.yml @@ -0,0 +1,10 @@ +name: CB Secret PR Scan + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + SecretScanning: + uses: chargebee/cb-secrets-scanner/.github/workflows/cb-secret-scan.yml@main + secrets: inherit From 6a9e83a781c8b79c095b5df90238ff2fef3bec92 Mon Sep 17 00:00:00 2001 From: Rohit Sharma Date: Mon, 21 Apr 2025 10:01:07 +0530 Subject: [PATCH 83/84] workflow updated --- .github/workflows/secret.scan.yml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/secret.scan.yml b/.github/workflows/secret.scan.yml index bcdb1c0..e08ac36 100644 --- a/.github/workflows/secret.scan.yml +++ b/.github/workflows/secret.scan.yml @@ -5,6 +5,28 @@ on: types: [opened, synchronize, reopened] jobs: - SecretScanning: - uses: chargebee/cb-secrets-scanner/.github/workflows/cb-secret-scan.yml@main - secrets: inherit + scan: + name: Secret PR Scan + runs-on: ubuntu-latest + steps: + - name: Get the file name changed in the PR + id: pr_files + run: | + changed_files=$(curl -s -H "Authorization: token ${{ + secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files | jq -r '.[].filename') + echo "Changed files: $changed_files" + + echo "CHANGED_FILES<> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: gitleaks/gitleaks-action@v2 + env: + GITLEAKS_ENABLE_UPLOAD_ARTIFACT: false + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + GITLEAKS_LICENSE: '${{ secrets.GITLEAKS_LICENSE }}' + GITLEAKS_ENABLE_SUMMARY: false From 1f914ee7358234e491c6cf476993f4bfae9cd5a4 Mon Sep 17 00:00:00 2001 From: Shrey Gupta Date: Mon, 16 Jun 2025 19:45:59 +0530 Subject: [PATCH 84/84] Message for legacy version added. --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba2bec0..80fd21c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# Chargebee Android +> [!WARNING] +> **Chargebee Android SDK** is part of Chargebee's legacy Mobile Subscription Solution and will not include the latest enhancements. We recommend using Chargebee's new [Omnichannel Subscriptions](https://www.chargebee.com/docs/billing/2.0/mobile-subscriptions/omnichannel-subscription-overview) solution for improved reliability and unified mobile subscriptions experience. +If you are a new customer, or considering to migrate new solution, please reach out to support@chargebee.com or contact your Technical Success Manager (TSM) for guidance. + +Chargebee Android (Legacy) +============= > [!NOTE] > #### Updates for Billing Library 5