Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Omnisub 5499 - Upgrade/downgrade subscription #98

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
79acc6c
feat: Added Change Product method in CBPurchase
cb-aravindradhakrishnan Jan 18, 2024
08d0006
feat: Added Change Product option
cb-aravindradhakrishnan Jan 18, 2024
942e453
feat: Created ChangeProduct params classwith oldProductId and prorati…
cb-aravindradhakrishnan Jan 24, 2024
94f2f6f
feat: Code refactored
cb-aravindradhakrishnan Jan 24, 2024
203dc88
feat: Return Valid errors
cb-aravindradhakrishnan Jan 24, 2024
e8cd2e6
feat: Added Code comments
cb-aravindradhakrishnan Jan 25, 2024
c6b1758
feat: Modified logs
cb-aravindradhakrishnan Jan 25, 2024
83b7702
feat: Replace empty string with empty optional
cb-aravindradhakrishnan Jan 27, 2024
e66769f
feat: Remove OTP callback on error
cb-aravindradhakrishnan Jan 27, 2024
08616b0
feat: Remove prorationMode from ChangeProductParams and set supported…
cb-aravindradhakrishnan Jan 29, 2024
484d8b9
Feat: Error handling
cb-aravindradhakrishnan Jan 29, 2024
27c85e6
feat: Rename changeProductParams props
cb-aravindradhakrishnan Jan 30, 2024
7b706ca
feat: Rename changeProductParams props
cb-aravindradhakrishnan Jan 30, 2024
56593af
feat: Proper error message
cb-aravindradhakrishnan Jan 30, 2024
62c3248
feat: Updated readme
cb-aravindradhakrishnan Jan 30, 2024
803326f
feat: Proper error message
cb-aravindradhakrishnan Jan 30, 2024
159809a
feat: Rename changeProductParams props
cb-aravindradhakrishnan Jan 30, 2024
7e0d6da
feat: Proper error message
cb-aravindradhakrishnan Jan 30, 2024
ecb90c4
feat: Reset currentProductId
cb-aravindradhakrishnan Jan 31, 2024
90f39e3
feat: Handled within subscription case
cb-aravindradhakrishnan Feb 5, 2024
2c98f6a
feat: Revert OTP callback on error
cb-aravindradhakrishnan Feb 6, 2024
86676e1
feat: Revert OTP callback on error
cb-aravindradhakrishnan Feb 6, 2024
f8b63b0
feat: Updated readme
cb-aravindradhakrishnan Feb 6, 2024
ba2d1a7
feat: Function name change
cb-aravindradhakrishnan Feb 6, 2024
e9c5422
feat: Updated readme
cb-aravindradhakrishnan Feb 6, 2024
418c231
feat: Add relevant error messages
cb-aravindradhakrishnan Feb 22, 2024
fb2ae76
feat: Update readme
cb-aravindradhakrishnan Feb 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ CBPurchase.purchaseProduct(purchaseProductParams = purchaseProductParams, custom
```
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.

### Upgrade or Downgrade Product
Pass the `ChangeProductParams` and `CBCustomer` to the following `ChangeProduct` function when the user chooses the product to purchase.

`CBCustomer` - **Optional object**. Although this is an optional object, we recommend passing the necessary customer details, such as `customerId`, `firstName`, `lastName`, and `email`. This ensures that the customer details in your database match the customer details in Chargebee. If the `customerId` is not passed in the customer's details, then the value of `customerId` will be the same as the `SubscriptionId` created in Chargebee.

**Note**: The `customer` parameter in the below code snippet is an instance of `CBCustomer` class that contains the details of the customer who wants to subscribe or buy the product.

```kotlin
val changeProductParams = ChangeProductParams(newProductParams, "currentProductId")
val cbCustomer = CBCustomer("customerId","firstName","lastName","email")
CBPurchase.changeProduct(changeProductParams = changeProductParams, customer = cbCustomer, object : CBCallback.PurchaseCallback<String>{
override fun onSuccess(result: ReceiptDetail, status:Boolean) {
Log.i(TAG, "$status")
Log.i(TAG, "${result.subscription_id}")
Log.i(TAG, "${result.plan_id}")
}
override fun onError(error: CBException) {
Log.e(TAG, "Error: ${error.message}")
}
})
```
The above function is designed to manage subscription upgrades or downgrades between base plans within a single subscription or across different subscriptions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be across different subscriptions?

### Invoke Manage Subscriptions in your App
The `showManageSubscriptionsSettings()` function is designed to invoke the Manage Subscriptions in your app using Chargebee's Android SDKs. `Chargebee.showManageSubscriptionsSettings()`, opens the Play Store App subscriptions settings page.

Expand Down
29 changes: 26 additions & 3 deletions app/src/main/java/com/chargebee/example/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -133,6 +134,9 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener {
CBMenu.GetProducts.value -> {
getProductIdFromCustomer()
}
CBMenu.ChangeProducts.value -> {
getCurrentAndNewProductIdFromCustomer()
}
CBMenu.SubsStatus.value,
CBMenu.SubsList.value -> {
val intent = Intent(this, SubscriptionActivity::class.java)
Expand All @@ -152,9 +156,10 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener {
}
}

private fun launchProductDetailsScreen(productDetails: String) {
private fun launchProductDetailsScreen(productDetails: String, currentProductId: String? = null) {
val intent = Intent(this, BillingActivity::class.java)
intent.putExtra(PRODUCTS_LIST_KEY, productDetails)
intent.putExtra(OLD_PRODUCT_ID, currentProductId)
this.startActivity(intent)
}

Expand Down Expand Up @@ -206,15 +211,33 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener {
dialog.show()
}

private fun getProductIdList(productIdList: ArrayList<String>) {
private fun getCurrentAndNewProductIdFromCustomer() {
val dialog = Dialog(this)
dialog.setContentView(R.layout.dialog_input_update_layout)
val productIds = dialog.findViewById<View>(R.id.productIdInput) as EditText
productIds.hint = "Please enter Product IDs(Comma separated)"
val currentProductId = dialog.findViewById<View>(R.id.currentProductIdInput) as EditText
currentProductId.hint = "Please enter Current Product ID"
val dialogButton = dialog.findViewById<View>(R.id.btn_ok) as Button
dialogButton.text = "Submit"
dialogButton.setOnClickListener {
val productIdList = productIds.text.toString().trim().split(",")
val currentProductId = currentProductId.text.toString().trim()
getProductIdList(productIdList.toCollection(ArrayList()), currentProductId)
dialog.dismiss()
}
dialog.show()
}

private fun getProductIdList(productIdList: ArrayList<String>, currentProductId: String? = null) {
CBPurchase.retrieveProducts(
this,
productIdList,
object : CBCallback.ListProductsCallback<ArrayList<CBProduct>> {
override fun onSuccess(productIDs: ArrayList<CBProduct>) {
CoroutineScope(Dispatchers.Main).launch {
if (productIDs.size > 0) {
launchProductDetailsScreen(gson.toJson(productIDs))
launchProductDetailsScreen(gson.toJson(productIDs), currentProductId)
} else {
alertSuccess("Items not available to buy")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,6 +18,7 @@
import com.chargebee.android.billingservice.OneTimeProductType;
import com.chargebee.android.billingservice.ProductType;
import com.chargebee.android.models.CBProduct;
import com.chargebee.android.models.ChangeProductParams;
import com.chargebee.android.models.PurchaseProductParams;
import com.chargebee.android.network.CBCustomer;
import com.chargebee.example.BaseActivity;
Expand All @@ -35,6 +37,7 @@
public class BillingActivity extends BaseActivity implements ProductListAdapter.ProductClickListener, ProgressBarListener {

private List<PurchaseProduct> purchaseProducts = null;
private String currentProductId = null;
private ProductListAdapter productListAdapter = null;
private LinearLayoutManager linearLayoutManager;
private RecyclerView mItemsRecyclerView = null;
Expand All @@ -53,6 +56,7 @@ protected void onCreate(Bundle savedInstanceState) {

mItemsRecyclerView = findViewById(R.id.rv_product_list);
String productDetails = getIntent().getStringExtra(PRODUCTS_LIST_KEY);
this.currentProductId = getIntent().getStringExtra(OLD_PRODUCT_ID);

if(productDetails != null) {
Gson gson = new Gson();
Expand Down Expand Up @@ -163,7 +167,11 @@ private void getCustomerID() {
dialog.dismiss();
}
} else {
purchaseProduct();
if (this.currentProductId != null){
changeProduct();
}else {
purchaseProduct();
}
dialog.dismiss();
}
});
Expand All @@ -189,6 +197,14 @@ private void purchaseProduct() {
this.billingViewModel.purchaseProduct(this, purchaseParams, cbCustomer);
}

private void changeProduct() {
showProgressDialog();
PurchaseProduct selectedPurchaseProduct = purchaseProducts.get(position);
PurchaseProductParams newProductParams = new PurchaseProductParams(selectedPurchaseProduct.getCbProduct(), selectedPurchaseProduct.getOfferToken());
ChangeProductParams changeProductParams = new ChangeProductParams(newProductParams, currentProductId);
this.billingViewModel.changeProduct(this, changeProductParams, cbCustomer);
}

private void purchaseNonSubscriptionProduct(OneTimeProductType productType) {
showProgressDialog();
CBProduct selectedProduct = purchaseProducts.get(position).getCbProduct();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ class BillingViewModel : ViewModel() {
})
}

fun changeProduct(context: Context, changeProductParams: ChangeProductParams, customer: CBCustomer) {
// Cache the product id in sharedPreferences and retry validating the receipt if in case server is not responding or no internet connection.
sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE)
CBPurchase.changeProduct(changeProductParams = changeProductParams, customer = customer, object : CBCallback.PurchaseCallback<String>{
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(changeProductParams.newProductParams.product.id)
validateReceipt(context = context, product = changeProductParams.newProductParams.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",
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/chargebee/example/util/CBMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/chargebee/example/util/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package com.chargebee.example.util

object Constants {
const val PRODUCTS_LIST_KEY = "products"
const val OLD_PRODUCT_ID = "currentProductId"
}
66 changes: 66 additions & 0 deletions app/src/main/res/layout/dialog_input_update_layout.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="300dp"
android:layout_height="300dp"
tools:context=".MainActivity">

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/planIdInputLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="96dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">

<EditText
android:id="@+id/productIdInput"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="13"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/currentPlanIdInputLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">

<EditText
android:id="@+id/currentProductIdInput"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="13"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Chargebee"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/btn_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="34dp"
android:text="Submit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/planIdInputLayout" />

</androidx.constraintlayout.widget.ConstraintLayout>
Loading