From 79acc6c4e9d11fea384b3265f23f214afa816de0 Mon Sep 17 00:00:00 2001 From: Aravind Radhakrishnan Date: Thu, 18 Jan 2024 13:35:21 +0530 Subject: [PATCH 01/27] feat: Added Change Product method in CBPurchase --- .../billingservice/BillingClientManager.kt | 93 +++++++++++++++++++ .../android/billingservice/CBPurchase.kt | 17 ++++ 2 files changed, 110 insertions(+) diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt index a59f2dd..936a08d 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -253,6 +253,99 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } + internal fun changeProduct( + purchaseProductParams: PurchaseProductParams, + oldProductId: String, + purchaseCallBack: CBCallback.PurchaseCallback + ) { + this.purchaseCallBack = purchaseCallBack + onConnected({ status -> + if (status) { + changeProduct(purchaseProductParams, oldProductId) + } else + purchaseCallBack.onError( + connectionError + ) + }, { error -> + purchaseCallBack.onError(error) + }) + + } + + private fun changeProduct(purchaseProductParams: PurchaseProductParams, oldProductId: String) { + this.purchaseProductParams = purchaseProductParams + val offerToken = purchaseProductParams.offerToken + + val queryProductDetails = arrayListOf(QueryProductDetailsParams.Product.newBuilder() + .setProductId(purchaseProductParams.product.id) + .setProductType(purchaseProductParams.product.type.value) + .build()) + + val productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(queryProductDetails).build() + + billingClient?.queryProductDetailsAsync( + productDetailsParams + ) { billingResult, productsDetail -> + if (billingResult.responseCode == OK && productsDetail != null) { + val productDetailsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productsDetail.first()) + offerToken?.let { productDetailsBuilder.setOfferToken(it) } + val productDetailsParamsList = + listOf(productDetailsBuilder.build()) + + val purchaseTransactionHistory = mutableListOf() + var oldPurchaseToken: String? = "" + queryAllSubsPurchaseHistory(ProductType.SUBS.value) { subscriptionHistory -> + purchaseTransactionHistory.addAll(subscriptionHistory ?: emptyList()) + val prevProduct: PurchaseTransaction? = purchaseTransactionHistory.find { it.productId.first() == oldProductId } + oldPurchaseToken = prevProduct?.purchaseToken + + val billingFlowParams = + BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList) + .setSubscriptionUpdateParams( + BillingFlowParams.SubscriptionUpdateParams.newBuilder() + .setOldPurchaseToken(oldPurchaseToken.toString()) + .setReplaceProrationMode( + BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION + ).build() + ).build() + + billingClient?.launchBillingFlow(mContext as Activity, billingFlowParams) + .takeIf { billingResult -> + billingResult?.responseCode != OK + }?.let { billingResult -> + Log.e(TAG, "Failed to launch billing flow $billingResult") + val billingError = CBException( + ErrorDetail( + message = GPErrorCode.LaunchBillingFlowError.errorMsg, + httpStatusCode = billingResult.responseCode + ) + ) + if (ProductType.SUBS == purchaseProductParams.product.type) { + purchaseCallBack?.onError( + billingError + ) + } else { + oneTimePurchaseCallback?.onError( + billingError + ) + } + } + } + } else { + Log.e(TAG, "Failed to fetch product :" + billingResult.responseCode) + if (ProductType.SUBS == purchaseProductParams.product.type) { + purchaseCallBack?.onError(throwCBException(billingResult)) + } else { + oneTimePurchaseCallback?.onError(throwCBException(billingResult)) + } + + } + } + + } + /** * 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. 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 079a2e0..ab0f118 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -77,6 +77,23 @@ object CBPurchase { }) } + fun changeProduct( + purchaseProductParams: PurchaseProductParams, customer: CBCustomer? = null, oldProductId: String, + callback: CBCallback.PurchaseCallback + ) { + this.customer = customer + changeProduct(purchaseProductParams, oldProductId, callback) + } + + private fun changeProduct(purchaseProductParams: PurchaseProductParams, oldProductId: String, callback: CBCallback.PurchaseCallback) { + isSDKKeyValid({ + log(customer, purchaseProductParams.product.id) + billingClientManager?.changeProduct(purchaseProductParams, oldProductId, callback) + }, { + callback.onError(it) + }) + } + /** * Buy the non-subscription product with/without customer data * @param [product] The product that wish to purchase From 08d0006cb36e847ba40abf5964fcec52d1b6e871 Mon Sep 17 00:00:00 2001 From: Aravind Radhakrishnan Date: Thu, 18 Jan 2024 13:37:06 +0530 Subject: [PATCH 02/27] feat: Added Change Product option --- .../com/chargebee/example/MainActivity.kt | 29 +++++++- .../example/billing/BillingActivity.java | 16 ++++- .../example/billing/BillingViewModel.kt | 25 +++++++ .../java/com/chargebee/example/util/CBMenu.kt | 1 + .../com/chargebee/example/util/Constants.kt | 1 + .../res/layout/dialog_input_update_layout.xml | 66 +++++++++++++++++++ 6 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/layout/dialog_input_update_layout.xml diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index a16cc99..0deb8f5 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -30,6 +30,7 @@ import com.chargebee.example.plan.PlansActivity import com.chargebee.example.subscription.SubscriptionActivity import com.chargebee.example.token.TokenizeActivity import com.chargebee.example.util.CBMenu +import com.chargebee.example.util.Constants.OLD_PRODUCT_ID import com.chargebee.example.util.Constants.PRODUCTS_LIST_KEY import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope @@ -133,6 +134,9 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { CBMenu.GetProducts.value -> { getProductIdFromCustomer() } + CBMenu.ChangeProducts.value -> { + getOldAndNewProductIdFromCustomer() + } CBMenu.SubsStatus.value, CBMenu.SubsList.value -> { val intent = Intent(this, SubscriptionActivity::class.java) @@ -152,9 +156,10 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { } } - private fun launchProductDetailsScreen(productDetails: String) { + private fun launchProductDetailsScreen(productDetails: String, oldProductId: String? = null) { val intent = Intent(this, BillingActivity::class.java) intent.putExtra(PRODUCTS_LIST_KEY, productDetails) + intent.putExtra(OLD_PRODUCT_ID, oldProductId) this.startActivity(intent) } @@ -206,7 +211,25 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { dialog.show() } - private fun getProductIdList(productIdList: ArrayList) { + private fun getOldAndNewProductIdFromCustomer() { + val dialog = Dialog(this) + dialog.setContentView(R.layout.dialog_input_update_layout) + val productIds = dialog.findViewById(R.id.productIdInput) as EditText + productIds.hint = "Please enter Product IDs(Comma separated)" + val oldProductId = dialog.findViewById(R.id.oldProductIdInput) as EditText + oldProductId.hint = "Please enter old Product ID" + val dialogButton = dialog.findViewById(R.id.btn_ok) as Button + dialogButton.text = "Submit" + dialogButton.setOnClickListener { + val productIdList = productIds.text.toString().trim().split(",") + val oldProductId = oldProductId.text.toString().trim() + getProductIdList(productIdList.toCollection(ArrayList()), oldProductId) + dialog.dismiss() + } + dialog.show() + } + + private fun getProductIdList(productIdList: ArrayList, oldProductId: String? = null) { CBPurchase.retrieveProducts( this, productIdList, @@ -214,7 +237,7 @@ 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), oldProductId) } else { alertSuccess("Items not available to buy") } diff --git a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java index 231ffa4..3820524 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -1,5 +1,6 @@ package com.chargebee.example.billing; +import static com.chargebee.example.util.Constants.OLD_PRODUCT_ID; import static com.chargebee.example.util.Constants.PRODUCTS_LIST_KEY; import android.app.Dialog; @@ -35,6 +36,7 @@ public class BillingActivity extends BaseActivity implements ProductListAdapter.ProductClickListener, ProgressBarListener { private List purchaseProducts = null; + private String oldProductId = null; private ProductListAdapter productListAdapter = null; private LinearLayoutManager linearLayoutManager; private RecyclerView mItemsRecyclerView = null; @@ -53,6 +55,7 @@ protected void onCreate(Bundle savedInstanceState) { mItemsRecyclerView = findViewById(R.id.rv_product_list); String productDetails = getIntent().getStringExtra(PRODUCTS_LIST_KEY); + this.oldProductId = getIntent().getStringExtra(OLD_PRODUCT_ID); if(productDetails != null) { Gson gson = new Gson(); @@ -163,7 +166,11 @@ private void getCustomerID() { dialog.dismiss(); } } else { - purchaseProduct(); + if (this.oldProductId != null){ + changeProduct(); + }else { + purchaseProduct(); + } dialog.dismiss(); } }); @@ -189,6 +196,13 @@ private void purchaseProduct() { this.billingViewModel.purchaseProduct(this, purchaseParams, cbCustomer); } + private void changeProduct() { + showProgressDialog(); + PurchaseProduct selectedPurchaseProduct = purchaseProducts.get(position); + PurchaseProductParams purchaseParams = new PurchaseProductParams(selectedPurchaseProduct.getCbProduct(), selectedPurchaseProduct.getOfferToken()); + this.billingViewModel.changeProduct(this, purchaseParams, cbCustomer, oldProductId); + } + private void purchaseNonSubscriptionProduct(OneTimeProductType productType) { showProgressDialog(); CBProduct selectedProduct = purchaseProducts.get(position).getCbProduct(); diff --git a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt index 1fdf2ee..4ca74b9 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -53,6 +53,31 @@ class BillingViewModel : ViewModel() { }) } + fun changeProduct(context: Context, purchaseProductParams: PurchaseProductParams, customer: CBCustomer, oldProductId: String) { + // Cache the product id in sharedPreferences and retry validating the receipt if in case server is not responding or no internet connection. + sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) + CBPurchase.changeProduct(purchaseProductParams = purchaseProductParams, customer = customer, oldProductId = oldProductId, object : CBCallback.PurchaseCallback{ + override fun onSuccess(result: ReceiptDetail, status:Boolean) { + Log.i(TAG, "Subscription ID: ${result.subscription_id}") + Log.i(TAG, "Plan ID: ${result.plan_id}") + productPurchaseResult.postValue(status) + } + override fun onError(error: CBException) { + try { + // Handled server not responding and offline + if (error.httpStatusCode!! in 500..599) { + storeInLocal(purchaseProductParams.product.id) + validateReceipt(context = context, product = purchaseProductParams.product) + } else { + cbException.postValue(error) + } + } catch (exp: Exception) { + Log.i(TAG, "Exception :${exp.message}") + } + } + }) + } + private fun validateReceipt(context: Context, product: CBProduct) { val customer = CBCustomer( id = "sync_receipt_android", 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 777dc73..d372e3d 100644 --- a/app/src/main/java/com/chargebee/example/util/CBMenu.kt +++ b/app/src/main/java/com/chargebee/example/util/CBMenu.kt @@ -10,6 +10,7 @@ enum class CBMenu(val value: String) { Tokenize("Tokenize"), ProductIDs("Get Google Play Product Identifiers"), GetProducts("Get Products"), + ChangeProducts("Change Product"), SubsStatus("Get Subscription Status"), SubsList("Get Subscriptions List"), GetEntitlements("Get Entitlements"), diff --git a/app/src/main/java/com/chargebee/example/util/Constants.kt b/app/src/main/java/com/chargebee/example/util/Constants.kt index 700be5d..7cf0522 100644 --- a/app/src/main/java/com/chargebee/example/util/Constants.kt +++ b/app/src/main/java/com/chargebee/example/util/Constants.kt @@ -2,4 +2,5 @@ package com.chargebee.example.util object Constants { const val PRODUCTS_LIST_KEY = "products" + const val OLD_PRODUCT_ID = "oldProductId" } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_input_update_layout.xml b/app/src/main/res/layout/dialog_input_update_layout.xml new file mode 100644 index 0000000..bb1cc36 --- /dev/null +++ b/app/src/main/res/layout/dialog_input_update_layout.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + +