From 6abcbb49a5b995119ba366afd9cc5e737b39fca6 Mon Sep 17 00:00:00 2001 From: cb-amutha Date: Mon, 1 May 2023 10:13:46 +0530 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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)) } } }