diff --git a/README.md b/README.md index 8a7905e..a7012f6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ # Chargebee Android + +> #### Updates for Billing Library 5 +> ***Note**: +> - SDK Version 2.0: This version uses Google Billing Library 5.2.1 APIs to fetch product information from the Google Play Console and make purchases. If you’re integrating Chargebee’s SDK for the first time, then use this version, and if you’re migrating from the older version of SDK to this version, follow the migration steps in this [document](https://www.chargebee.com/docs/2.0/mobile-playstore-billing-library-5.html). +> - SDK Version 1.2.0: This [version](https://github.com/chargebee/chargebee-android/tree/1.x.x) includes Billing Library 5.2.1 but still uses Billing Library 4.0 APIs to fetch product information from the Google Play Console and make purchases. This will enable you to list or update your Android app on the store without any warnings from Google and give you enough time to migrate to version 2.0. +> - SDK Version 1.1.0: This and less than this version of SDKs use billing library 4.0 APIs that are deprecated by Google. Therefore, it is highly recommended that you upgrade your app and integrate it with SDK version 1.2.0 and above. + + This is Chargebee’s Android Software Development Kit (SDK). This SDK makes it efficient and comfortable to build a seamless subscription experience in your Android app. Post-installation, initialization, and authentication with the Chargebee site, this SDK will support the following process. @@ -21,7 +29,7 @@ The following requirements must be set up before installing Chargebee’s Androi The `Chargebee-Android` SDK can be installed by adding below dependency to the `build.gradle` file: ```kotlin -implementation 'com.chargebee:chargebee-android:1.2.0' +implementation 'com.chargebee:chargebee-android:2.0.0-beta-1' ``` ## Example project @@ -55,9 +63,21 @@ To configure the Chargebee Android SDK for completing and managing In-App Purcha ```kotlin import com.chargebee.android.Chargebee -Chargebee.configure(site= "your-site", - publishableApiKey= "api_key", - sdkKey= "sdk_key",packageName = "packageName") +Chargebee.configure( + site = "your-site", + publishableApiKey = "api-key", + sdkKey = "sdk-key", + packageName = "your-package" +) { + when (it) { + is ChargebeeResult.Success -> { + // Success + } + is ChargebeeResult.Error -> { + // Error + } + } +} ``` ### Configuration for credit card using tokenization To configure SDK only for tokenizing credit card details, follow these steps. @@ -69,7 +89,7 @@ To configure SDK only for tokenizing credit card details, follow these steps. ```kotlin import com.chargebee.android.Chargebee -Chargebee.configure(site = "your-site", publishableApiKey = "api_key") +Chargebee.configure(site = "your-site", publishableApiKey = "api-key") ``` ## Integration @@ -86,7 +106,7 @@ The following section describes how to use the SDK to integrate In-App Purchase Every In-App Purchase subscription product that you configure in your Play Store account, can be configured in Chargebee as a Plan. Start by retrieving the Google IAP Product IDs from your Chargebee account. ```kotlin -CBPurchase.retrieveProductIdentifers(queryParam) { +CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { Log.i(TAG, "List of Product Identifiers: $it") @@ -106,29 +126,29 @@ The above function will determine your product catalog version in Chargebee and Retrieve the Google IAP Product using the following function. ```kotlin -CBPurchase.retrieveProducts(this, productIdList= "[Product ID's from Google Play Console]", - object : CBCallback.ListProductsCallback> { - override fun onSuccess(productDetails: ArrayList) { +CBPurchase.retrieveProducts(activity, productIdList= ["Product ID's from Google Play Console"], + object : CBCallback.ListProductsCallback> { + override fun onSuccess(productDetails: ArrayList) { Log.i(TAG, "List of Products: $productDetails") } override fun onError(error: CBException) { Log.e(TAG, "Error: ${error.message}") - // Handle error here } }) - ``` You can present any of the above products to your users for them to purchase. ### Buy or Subscribe Product -Pass the `CBProduct` and `CBCustomer` objects to the following function when the user chooses the product to purchase. +Pass the `PurchaseProductParams`, `CBCustomer` and `OfferToken` to the following function when the user chooses the product to purchase. `CBCustomer` - **Optional object**. Although this is an optional object, we recommend passing the necessary customer details, such as `customerId`, `firstName`, `lastName`, and `email` if it is available before the user subscribes to your App. This ensures that the customer details in your database match the customer details in Chargebee. If the `customerId` is not passed in the customer's details, then the value of `customerId` will be the same as the `SubscriptionId` created in Chargebee. **Note**: The `customer` parameter in the below code snippet is an instance of `CBCustomer` class that contains the details of the customer who wants to subscribe or buy the product. ```kotlin -CBPurchase.purchaseProduct(product=CBProduct, customer=CBCustomer, object : CBCallback.PurchaseCallback{ +val purchaseParams = PurchaseProductParams(selectedCBProduct, "selectedOfferToken") +val cbCustomer = CBCustomer("customerId","firstName","lastName","email") +CBPurchase.purchaseProduct(purchaseProductParams = purchaseProductParams, customer = cbCustomer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { Log.i(TAG, "$status") Log.i(TAG, "${result.subscription_id}") @@ -162,7 +182,7 @@ CBPurchase.purchaseNonSubscriptionProduct(product = CBProduct, customer = CBCust The given code defines a function named `purchaseNonSubscriptionProduct` in the CBPurchase class, which takes four input parameters: -- `product`: An instance of `CBProduct` class, initialized with a `SkuDetails` instance representing the product to be purchased from the Google Play Store. +- `product`: An instance of `CBProduct` class, representing the product to be purchased from the Google Play Store. - `customer`: Optional. An instance of `CBCustomer` class, initialized with the customer's details such as `customerId`, `firstName`, `lastName`, and `email`. - `productType`: An enum instance of `productType` type, indicating the type of product to be purchased. It can be either .`consumable`, or `non_consumable`. - `callback`: The `OneTimePurchaseCallback` listener will be invoked when product purchase completes. diff --git a/app/build.gradle b/app/build.gradle index 67e8723..0f20f06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,16 +3,16 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 30 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { applicationId "com.chargebee.example" - minSdkVersion 21 - targetSdkVersion 30 - versionCode 3 + minSdkVersion 24 + targetSdkVersion 33 + versionCode 6 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -40,7 +40,7 @@ dependencies { implementation 'com.google.android.material:material:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation 'com.google.code.gson:gson:2.8.8' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89f51ce..0ff6c0a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,12 +17,13 @@ - - - - - - + + + + + + diff --git a/app/src/main/java/com/chargebee/example/ExampleApplication.kt b/app/src/main/java/com/chargebee/example/ExampleApplication.kt index 5e7a473..fc678ac 100644 --- a/app/src/main/java/com/chargebee/example/ExampleApplication.kt +++ b/app/src/main/java/com/chargebee/example/ExampleApplication.kt @@ -2,7 +2,6 @@ package com.chargebee.example import android.app.Application import android.content.Context -import com.chargebee.android.Chargebee import android.content.SharedPreferences import android.util.Log import com.chargebee.android.billingservice.CBCallback @@ -18,7 +17,7 @@ import com.chargebee.example.util.NetworkUtil class ExampleApplication : Application(), NetworkUtil.NetworkListener { private lateinit var networkUtil: NetworkUtil - private lateinit var sharedPreference: SharedPreferences + private var sharedPreference: SharedPreferences? = null lateinit var mContext: Context private val customer = CBCustomer( id = "sync_receipt_android", @@ -36,8 +35,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { } override fun onNetworkConnectionAvailable() { - Chargebee.configure(site = "", publishableApiKey= "",sdkKey= "", packageName = this.packageName) - val productId = sharedPreference.getString("productId", "") + val productId = sharedPreference?.getString("productId", "") if (productId?.isNotEmpty() == true) { val productList = ArrayList() productList.add(productId) @@ -55,7 +53,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { productIdList, object : CBCallback.ListProductsCallback> { override fun onSuccess(productIDs: ArrayList) { - if (productIDs.first().productType == ProductType.SUBS) + if (productIDs.first().type == ProductType.SUBS) validateReceipt(mContext, productIDs.first()) else validateNonSubscriptionReceipt(mContext, productIDs.first()) @@ -76,8 +74,8 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { completionCallback = object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { // Clear the local cache once receipt validation success - val editor = sharedPreference.edit() - editor.clear().apply() + val editor = sharedPreference?.edit() + editor?.clear()?.apply() Log.i(javaClass.simpleName, "Subscription ID: ${result.subscription_id}") Log.i(javaClass.simpleName, "Plan ID: ${result.plan_id}") Log.i(javaClass.simpleName, "Customer ID: ${result.customer_id}") @@ -99,8 +97,8 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener { completionCallback = object : CBCallback.OneTimePurchaseCallback { override fun onSuccess(result: NonSubscription, status: Boolean) { // Clear the local cache once receipt validation success - val editor = sharedPreference.edit() - editor.clear().apply() + val editor = sharedPreference?.edit() + editor?.clear()?.apply() Log.i(javaClass.simpleName, "Subscription ID: ${result.invoiceId}") Log.i(javaClass.simpleName, "Plan ID: ${result.chargeId}") Log.i(javaClass.simpleName, "Customer ID: ${result.customerId}") diff --git a/app/src/main/java/com/chargebee/example/MainActivity.kt b/app/src/main/java/com/chargebee/example/MainActivity.kt index b158d4d..a16cc99 100644 --- a/app/src/main/java/com/chargebee/example/MainActivity.kt +++ b/app/src/main/java/com/chargebee/example/MainActivity.kt @@ -16,8 +16,8 @@ import androidx.recyclerview.widget.RecyclerView import com.chargebee.android.Chargebee import com.chargebee.android.billingservice.CBCallback import com.chargebee.android.billingservice.CBPurchase -import com.chargebee.android.models.CBRestoreSubscription import com.chargebee.android.exceptions.CBException +import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.CBProduct import com.chargebee.example.adapter.ListItemsAdapter import com.chargebee.example.addon.AddonActivity @@ -173,11 +173,20 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener { ) && !TextUtils.isEmpty(sdkKeyEditText.text.toString()) ) Chargebee.configure( - siteNameEditText.text.toString(), - apiKeyEditText.text.toString(), - true, - sdkKeyEditText.text.toString(), this.packageName - ) + site = siteNameEditText.text.toString(), + publishableApiKey = apiKeyEditText.text.toString(), + sdkKey = sdkKeyEditText.text.toString(), + packageName = this.packageName + ) { + when (it) { + is ChargebeeResult.Success -> { + Log.i(javaClass.simpleName, "Configured") + } + is ChargebeeResult.Error -> { + Log.e(javaClass.simpleName, " Failed") + } + } + } } builder.show() } diff --git a/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java b/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java index 4bba7c2..95da11e 100644 --- a/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java +++ b/app/src/main/java/com/chargebee/example/adapter/ProductListAdapter.java @@ -7,19 +7,26 @@ import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; +import com.chargebee.android.billingservice.ProductType; import com.chargebee.android.models.CBProduct; +import com.chargebee.android.models.PricingPhase; +import com.chargebee.android.models.SubscriptionOffer; import com.chargebee.example.R; + +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; public class ProductListAdapter extends RecyclerView.Adapter { - private List mProductsList; + private List purchaseProducts; private ProductListAdapter.ProductClickListener mClickListener; private Context mContext = null; + private PurchaseProduct selectedProduct = null; - public ProductListAdapter(Context context, List mProductsList, ProductClickListener mClickListener) { + public ProductListAdapter(Context context, List purchaseProducts, ProductClickListener mClickListener) { mContext = context; - this.mProductsList = mProductsList; + this.purchaseProducts = purchaseProducts; this.mClickListener = mClickListener; } @@ -31,10 +38,12 @@ public ProductListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int vi @Override public void onBindViewHolder(ProductListAdapter.ViewHolder holder, int position) { - CBProduct products = mProductsList.get(position); - holder.mTextViewTitle.setText(products.getProductId()); - holder.mTextViewPrice.setText(products.getProductPrice()); - if (products.getSubStatus()) { + PurchaseProduct purchaseProduct = purchaseProducts.get(position); + holder.mTextViewTitle.setText(purchaseProduct.getProductId() + " "+ purchaseProduct.getBasePlanId()); + holder.mTextViewPrice.setText(purchaseProduct.getPrice()); + boolean isSubscriptionProductSelected = selectedProduct != null && selectedProduct.getCbProduct().getType().equals(ProductType.SUBS) && selectedProduct.getOfferToken().equals(purchaseProduct.getOfferToken()); + boolean isOtpProductSelected = selectedProduct != null && selectedProduct.getCbProduct().getType().equals(ProductType.INAPP) && selectedProduct.getProductId().equals(purchaseProduct.getProductId()); + if (isSubscriptionProductSelected || isOtpProductSelected) { holder.mTextViewSubscribe.setText(R.string.status_subscribed); holder.mTextViewSubscribe.setTextColor(mContext.getResources().getColor(R.color.success_green)); }else { @@ -46,7 +55,7 @@ public void onBindViewHolder(ProductListAdapter.ViewHolder holder, int position) @Override public int getItemCount() { - return mProductsList.size(); + return purchaseProducts.size(); } public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { @@ -63,6 +72,7 @@ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickL @Override public void onClick(View view) { if (mClickListener != null) { + selectedProduct = purchaseProducts.get(getAdapterPosition()); mClickListener.onProductClick(view, getAdapterPosition()); } } diff --git a/app/src/main/java/com/chargebee/example/adapter/PurchaseProduct.java b/app/src/main/java/com/chargebee/example/adapter/PurchaseProduct.java new file mode 100644 index 0000000..7d152b8 --- /dev/null +++ b/app/src/main/java/com/chargebee/example/adapter/PurchaseProduct.java @@ -0,0 +1,58 @@ +package com.chargebee.example.adapter; + +import com.chargebee.android.models.CBProduct; +import com.chargebee.android.models.PricingPhase; +import com.chargebee.android.models.SubscriptionOffer; + +public class PurchaseProduct { + private final String productId; + private final CBProduct cbProduct; + private final String basePlanId; + private final String offerId; + private final String offerToken; + private final String price; + + public PurchaseProduct(CBProduct cbProduct, SubscriptionOffer subscriptionOffer) { + this(cbProduct.getId(), cbProduct, subscriptionOffer.getBasePlanId(), subscriptionOffer.getOfferId(), + subscriptionOffer.getOfferToken(), subscriptionOffer.getPricingPhases().get(0).getFormattedPrice()); + } + + public PurchaseProduct(CBProduct cbProduct, PricingPhase oneTimePurchaseOffer) { + this(cbProduct.getId(), cbProduct, + null, null, + null, oneTimePurchaseOffer.getFormattedPrice()); + } + + public PurchaseProduct(String id, CBProduct cbProduct, String basePlanId, String offerId, String offerToken, String formattedPrice) { + this.productId = id; + this.cbProduct = cbProduct; + this.basePlanId = basePlanId; + this.offerId = offerId; + this.offerToken = offerToken; + this.price = formattedPrice; + } + + public String getProductId() { + return productId; + } + + public String getBasePlanId() { + return basePlanId; + } + + public String getOfferId() { + return offerId; + } + + public String getOfferToken() { + return offerToken; + } + + public String getPrice() { + return price; + } + + public CBProduct getCbProduct() { + return cbProduct; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java index 753151b..231ffa4 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingActivity.java +++ b/app/src/main/java/com/chargebee/example/billing/BillingActivity.java @@ -1,31 +1,40 @@ package com.chargebee.example.billing; +import static com.chargebee.example.util.Constants.PRODUCTS_LIST_KEY; + import android.app.Dialog; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; + import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; + import com.chargebee.android.ProgressBarListener; import com.chargebee.android.billingservice.OneTimeProductType; import com.chargebee.android.billingservice.ProductType; import com.chargebee.android.models.CBProduct; +import com.chargebee.android.models.PurchaseProductParams; import com.chargebee.android.network.CBCustomer; import com.chargebee.example.BaseActivity; import com.chargebee.example.R; import com.chargebee.example.adapter.ProductListAdapter; +import com.chargebee.example.adapter.PurchaseProduct; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; + import java.lang.reflect.Type; import java.util.ArrayList; -import static com.chargebee.example.util.Constants.PRODUCTS_LIST_KEY; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; public class BillingActivity extends BaseActivity implements ProductListAdapter.ProductClickListener, ProgressBarListener { - private ArrayList productList = null; + private List purchaseProducts = null; private ProductListAdapter productListAdapter = null; private LinearLayoutManager linearLayoutManager; private RecyclerView mItemsRecyclerView = null; @@ -48,10 +57,14 @@ protected void onCreate(Bundle savedInstanceState) { if(productDetails != null) { Gson gson = new Gson(); Type listType = new TypeToken>() {}.getType(); - productList = gson.fromJson(productDetails, listType); + List productList = gson.fromJson(productDetails, listType); + this.purchaseProducts = productList.stream() + .map(x -> toList(x)) + .flatMap(List::stream) + .collect(Collectors.toList()); } - productListAdapter = new ProductListAdapter(this,productList, this); + productListAdapter = new ProductListAdapter(this, purchaseProducts, this); linearLayoutManager = new LinearLayoutManager(this); mItemsRecyclerView.setLayoutManager(linearLayoutManager); mItemsRecyclerView.setItemAnimator(new DefaultItemAnimator()); @@ -150,8 +163,7 @@ private void getCustomerID() { dialog.dismiss(); } } else { - purchaseProduct(customerId); - // purchaseProduct(); + purchaseProduct(); dialog.dismiss(); } }); @@ -167,26 +179,23 @@ private boolean checkProductTypeFiled(){ } private boolean isOneTimeProduct(){ - return productList.get(position).getProductType() == ProductType.INAPP; - } - - private void purchaseProduct(String customerId) { - showProgressDialog(); - this.billingViewModel.purchaseProduct(this, productList.get(position), customerId); + return purchaseProducts.get(position).getCbProduct().getType() == ProductType.INAPP; } private void purchaseProduct() { showProgressDialog(); - this.billingViewModel.purchaseProduct(this, productList.get(position), cbCustomer); + PurchaseProduct selectedPurchaseProduct = purchaseProducts.get(position); + PurchaseProductParams purchaseParams = new PurchaseProductParams(selectedPurchaseProduct.getCbProduct(), selectedPurchaseProduct.getOfferToken()); + this.billingViewModel.purchaseProduct(this, purchaseParams, cbCustomer); } private void purchaseNonSubscriptionProduct(OneTimeProductType productType) { showProgressDialog(); - this.billingViewModel.purchaseNonSubscriptionProduct(this, productList.get(position), cbCustomer, productType); + CBProduct selectedProduct = purchaseProducts.get(position).getCbProduct(); + this.billingViewModel.purchaseNonSubscriptionProduct(this, selectedProduct, cbCustomer, productType); } private void updateSubscribeStatus(){ - productList.get(position).setSubStatus(true); productListAdapter.notifyDataSetChanged(); } @@ -200,4 +209,12 @@ public void onShowProgressBar() { public void onHideProgressBar() { hideProgressDialog(); } + private List toList(CBProduct cbProduct) { + if(cbProduct.getType() == ProductType.SUBS) { + return cbProduct.getSubscriptionOffers().stream() + .map(x -> new PurchaseProduct(cbProduct, x)).collect(Collectors.toList()); + } else { + return Arrays.asList(new PurchaseProduct(cbProduct, cbProduct.getOneTimePurchaseOffer())); + } + } } diff --git a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt index 1c66c00..1fdf2ee 100644 --- a/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt +++ b/app/src/main/java/com/chargebee/example/billing/BillingViewModel.kt @@ -28,10 +28,10 @@ class BillingViewModel : ViewModel() { private lateinit var sharedPreference : SharedPreferences var restorePurchaseResult: MutableLiveData> = MutableLiveData() - fun purchaseProduct(context: Context,product: CBProduct, customer: CBCustomer) { + fun purchaseProduct(context: Context, purchaseProductParams: PurchaseProductParams, customer: CBCustomer) { // Cache the product id in sharedPreferences and retry validating the receipt if in case server is not responding or no internet connection. sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) - CBPurchase.purchaseProduct(product, customer, object : CBCallback.PurchaseCallback{ + CBPurchase.purchaseProduct(purchaseProductParams = purchaseProductParams, customer = customer, object : CBCallback.PurchaseCallback{ override fun onSuccess(result: ReceiptDetail, status:Boolean) { Log.i(TAG, "Subscription ID: ${result.subscription_id}") Log.i(TAG, "Plan ID: ${result.plan_id}") @@ -41,30 +41,8 @@ class BillingViewModel : ViewModel() { try { // Handled server not responding and offline if (error.httpStatusCode!! in 500..599) { - storeInLocal(product.productId) - validateReceipt(context = context, product = product) - } else { - cbException.postValue(error) - } - } catch (exp: Exception) { - Log.i(TAG, "Exception :${exp.message}") - } - } - }) - } - fun purchaseProduct(context: Context, product: CBProduct, customerId: String) { - sharedPreference = context.getSharedPreferences("PREFERENCE_NAME",Context.MODE_PRIVATE) - CBPurchase.purchaseProduct(product, customerId, object : CBCallback.PurchaseCallback{ - override fun onSuccess(result: ReceiptDetail, status:Boolean) { - Log.i(TAG, "Subscription ID: ${result.subscription_id}") - Log.i(TAG, "Plan ID: ${result.plan_id}") - productPurchaseResult.postValue(status) - } - override fun onError(error: CBException) { - try { - if (error.httpStatusCode!! in 500..599) { - storeInLocal(product.productId) - validateReceipt(context = context, product = product) + storeInLocal(purchaseProductParams.product.id) + validateReceipt(context = context, product = purchaseProductParams.product) } else { cbException.postValue(error) } @@ -107,7 +85,7 @@ class BillingViewModel : ViewModel() { } fun retrieveProductIdentifers(queryParam: Array){ - CBPurchase.retrieveProductIdentifers(queryParam) { + CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { Log.i(TAG, "List of Product Identifiers: $it") @@ -197,7 +175,7 @@ class BillingViewModel : ViewModel() { try { // Handled server not responding and offline if (error.httpStatusCode!! in 500..599) { - storeInLocal(product.productId) + storeInLocal(product.id) validateNonSubscriptionReceipt(context = context, product = product, productType = productType) } else { cbException.postValue(error) diff --git a/app/src/main/java/com/chargebee/example/items/ItemActivity.kt b/app/src/main/java/com/chargebee/example/items/ItemActivity.kt index c015368..afb345f 100644 --- a/app/src/main/java/com/chargebee/example/items/ItemActivity.kt +++ b/app/src/main/java/com/chargebee/example/items/ItemActivity.kt @@ -6,10 +6,8 @@ import android.widget.Button import android.widget.EditText import android.widget.TextView import androidx.lifecycle.Observer -import com.chargebee.android.ErrorDetail import com.chargebee.example.BaseActivity import com.chargebee.example.R -import com.google.gson.Gson class ItemActivity: BaseActivity() { diff --git a/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt b/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt index 921588e..9f1eb21 100644 --- a/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt +++ b/app/src/main/java/com/chargebee/example/items/ItemsActivity.kt @@ -1,7 +1,6 @@ package com.chargebee.example.items import android.os.Bundle -import android.util.Log import android.view.View import android.widget.TextView import androidx.lifecycle.Observer @@ -9,11 +8,9 @@ import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.chargebee.android.Chargebee -import com.chargebee.android.ErrorDetail import com.chargebee.example.BaseActivity import com.chargebee.example.R import com.chargebee.example.adapter.ItemsAdapter -import com.google.gson.Gson class ItemsActivity : BaseActivity(), ItemsAdapter.ItemClickListener { diff --git a/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java b/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java index c53cefe..a211878 100644 --- a/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java +++ b/app/src/main/java/com/chargebee/example/plan/PlanInJavaActivity.java @@ -6,12 +6,8 @@ import android.widget.EditText; import android.widget.TextView; -import com.chargebee.android.ErrorDetail; -import com.chargebee.android.models.Plan; import com.chargebee.example.BaseActivity; import com.chargebee.example.R; -import com.google.gson.Gson; -import com.google.gson.JsonObject; public class PlanInJavaActivity extends BaseActivity { diff --git a/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt b/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt index 4d020bb..7e6890c 100644 --- a/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt +++ b/app/src/main/java/com/chargebee/example/plan/PlansActivity.kt @@ -8,12 +8,9 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.chargebee.android.Chargebee -import com.chargebee.android.ErrorDetail import com.chargebee.example.BaseActivity import com.chargebee.example.R import com.chargebee.example.adapter.ItemsAdapter -import com.google.gson.Gson class PlansActivity : BaseActivity(), ItemsAdapter.ItemClickListener { diff --git a/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt b/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt index c8f9c5c..f9b1dbb 100644 --- a/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt +++ b/app/src/main/java/com/chargebee/example/subscription/SubscriptionActivity.kt @@ -3,7 +3,6 @@ package com.chargebee.example.subscription import android.os.Bundle import android.util.Log import android.widget.Button -import com.chargebee.android.Chargebee import com.chargebee.example.BaseActivity import com.chargebee.example.R import com.chargebee.example.billing.BillingViewModel @@ -39,7 +38,7 @@ class SubscriptionActivity : BaseActivity() { Log.i(javaClass.simpleName, "Subscriptions by using queryParams: $it") if(it?.size!! >0) { val subscriptionStatus = - it?.get(0)?.cb_subscription?.status + "\nPlan Price : " + it?.get(0)?.cb_subscription?.plan_amount; + (it?.get(0)?.cb_subscription?.status ?: "") + "\nPlan Price : " + it?.get(0)?.cb_subscription?.plan_amount; alertSuccess(subscriptionStatus) }else{ alertSuccess("Subscriptions not found in Chargebee System") diff --git a/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt b/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt index 3847ac1..525f918 100644 --- a/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt +++ b/app/src/main/java/com/chargebee/example/token/TokenViewModel.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.chargebee.android.Chargebee -import com.chargebee.android.models.Token import com.chargebee.android.exceptions.InvalidRequestException import com.chargebee.android.exceptions.OperationFailedException import com.chargebee.android.exceptions.PaymentException diff --git a/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt b/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt index 0e6a4b1..033de23 100644 --- a/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt +++ b/app/src/main/java/com/chargebee/example/token/TokenizeActivity.kt @@ -5,7 +5,6 @@ import android.util.Log import android.widget.Button import android.widget.EditText import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import com.chargebee.android.models.Card import com.chargebee.android.models.PaymentDetail diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 43cdfa6..931cca5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -22,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" - android:text="Chargebee Examples" + android:text="Chargebee Examples - V1.0" android:textSize="24sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" diff --git a/build.gradle b/build.gradle index a2f6fcb..129bae1 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.2.2" + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/chargebee/build.gradle b/chargebee/build.gradle index 7b2d19a..ffe3cde 100644 --- a/chargebee/build.gradle +++ b/chargebee/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 1 - versionName "1.2.0" + versionName "2.0.0-beta-1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -52,10 +52,10 @@ dependencies { // Mockito testImplementation 'org.mockito:mockito-core:2.23.0' - testImplementation 'androidx.test:core:1.4.0' - testImplementation 'org.json:json:20140107' + testImplementation 'androidx.test:core:1.4.0' + testImplementation 'androidx.test.ext:junit:1.1.1' } diff --git a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt index 763ee1f..a7995a6 100644 --- a/chargebee/src/main/java/com/chargebee/android/Chargebee.kt +++ b/chargebee/src/main/java/com/chargebee/android/Chargebee.kt @@ -35,7 +35,7 @@ object Chargebee { var appName: String = "Chargebee" var environment: String = "cb_android_sdk" const val platform: String = "Android" - const val sdkVersion: String = "1.2.0" + const val sdkVersion: String = "2.0.0-beta-1" const val limit: String = "100" private const val PLAY_STORE_SUBSCRIPTION_URL = "https://play.google.com/store/account/subscriptions" diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt index ce7912b..a59f2dd 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/BillingClientManager.kt @@ -9,14 +9,16 @@ import com.android.billingclient.api.* import com.android.billingclient.api.BillingClient.BillingResponseCode.* import com.chargebee.android.ErrorDetail import com.chargebee.android.billingservice.BillingErrorCode.Companion.throwCBException -import com.chargebee.android.models.PurchaseTransaction import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.CBNonSubscriptionResponse import com.chargebee.android.models.CBProduct +import com.chargebee.android.models.PricingPhase +import com.chargebee.android.models.PurchaseProductParams +import com.chargebee.android.models.PurchaseTransaction +import com.chargebee.android.models.SubscriptionOffer import com.chargebee.android.network.CBReceiptResponse import com.chargebee.android.restore.CBRestorePurchaseManager -import kotlin.collections.ArrayList class BillingClientManager(context: Context) : PurchasesUpdatedListener { @@ -25,9 +27,8 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { var mContext: Context? = context private val handler = Handler(Looper.getMainLooper()) private var purchaseCallBack: CBCallback.PurchaseCallback? = null - private val skusWithSkuDetails = arrayListOf() private val TAG = javaClass.simpleName - lateinit var product: CBProduct + private lateinit var purchaseProductParams: PurchaseProductParams private lateinit var restorePurchaseCallBack: CBCallback.RestorePurchaseCallback private var oneTimePurchaseCallback: CBCallback.OneTimePurchaseCallback? = null @@ -36,12 +37,12 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } internal fun retrieveProducts( - skuList: ArrayList, callBack: CBCallback.ListProductsCallback> + products: ArrayList, callBack: CBCallback.ListProductsCallback> ) { val productsList = ArrayList() - retrieveProducts(ProductType.SUBS.value, skuList, { subsProductsList -> + retrieveProducts(BillingClient.ProductType.SUBS, products, { subsProductsList -> productsList.addAll(subsProductsList) - retrieveProducts(ProductType.INAPP.value, skuList, { inAppProductsList -> + retrieveProducts(BillingClient.ProductType.INAPP, products, { inAppProductsList -> productsList.addAll(inAppProductsList) callBack.onSuccess(productsList) }, { error -> @@ -53,13 +54,13 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } internal fun retrieveProducts( - @BillingClient.SkuType skuType: String, - skuList: ArrayList, response: (ArrayList) -> Unit, + @BillingClient.ProductType productType: String, + products: ArrayList, response: (ArrayList) -> Unit, errorDetail: (CBException) -> Unit ) { onConnected({ status -> if (status) - loadProductDetails(skuType, skuList, { + loadProductDetails(productType, products, { response(it) }, { errorDetail(it) @@ -72,39 +73,37 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { errorDetail(error) }) } + /* Get the SKU/Products from Play Console */ private fun loadProductDetails( - @BillingClient.SkuType skuType: String, - skuList: ArrayList, + @BillingClient.ProductType productType: String, + products: ArrayList, response: (ArrayList) -> Unit, errorDetail: (CBException) -> Unit ) { try { - val params = SkuDetailsParams - .newBuilder() - .setSkusList(skuList) - .setType(skuType) - .build() - billingClient?.querySkuDetailsAsync( - params - ) { billingResult, skuDetailsList -> - if (billingResult.responseCode == OK && skuDetailsList != null) { + val queryProductDetails = products.map { + QueryProductDetailsParams.Product.newBuilder() + .setProductId(it) + .setProductType(productType) + .build() + } + + val productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(queryProductDetails).build() + + billingClient?.queryProductDetailsAsync( + productDetailsParams + ) { billingResult, productsDetail -> + if (billingResult.responseCode == OK && productsDetail != null) { try { - skusWithSkuDetails.clear() - for (skuProduct in skuDetailsList) { - val product = CBProduct( - skuProduct.sku, - skuProduct.title, - skuProduct.price, - skuProduct, - false, - ProductType.getProductType(skuProduct.type) - ) - skusWithSkuDetails.add(product) + val cbProductDetails = arrayListOf() + for (productDetail in productsDetail) { + val cbProduct = convertToCbProduct(productDetail) + cbProductDetails.add(cbProduct) } - Log.i(TAG, "Product details :$skusWithSkuDetails") - response(skusWithSkuDetails) + Log.i(TAG, "Product details :$cbProductDetails") + response(cbProductDetails) } catch (ex: CBException) { errorDetail( CBException( @@ -128,16 +127,62 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { errorDetail(CBException(ErrorDetail(message = "${exp.message}"))) } } + private fun convertToCbProduct(productDetail: ProductDetails): CBProduct { + val subscriptionOffers = subscriptionOffers(productDetail.subscriptionOfferDetails) + val oneTimePurchaseOffer = oneTimePurchaseOffer(productDetail.oneTimePurchaseOfferDetails) + return CBProduct( + productDetail.productId, + productDetail.title, + productDetail.description, + ProductType.getProductType(productDetail.productType), + subscriptionOffers, + oneTimePurchaseOffer + ) + } + + private fun oneTimePurchaseOffer(oneTimePurchaseOfferDetails: ProductDetails.OneTimePurchaseOfferDetails?): PricingPhase? { + return oneTimePurchaseOfferDetails?.let { + return PricingPhase(oneTimePurchaseOfferDetails.formattedPrice, + oneTimePurchaseOfferDetails.priceAmountMicros, + oneTimePurchaseOfferDetails.priceCurrencyCode) + } + } + + private fun subscriptionOffers(subscriptionOfferDetails: List?): List? { + return subscriptionOfferDetails?.let { it.map { i -> subscriptionOffer(i) } } + } + + private fun subscriptionOffer(subscriptionOfferDetail: ProductDetails.SubscriptionOfferDetails): SubscriptionOffer { + val pricingPhases = pricingPhases(subscriptionOfferDetail.pricingPhases) + return SubscriptionOffer( + subscriptionOfferDetail.basePlanId, + subscriptionOfferDetail.offerId, + subscriptionOfferDetail.offerToken, + pricingPhases + ) + } + + private fun pricingPhases(pricingPhases: ProductDetails.PricingPhases): List { + return pricingPhases.pricingPhaseList.map { + PricingPhase( + it.formattedPrice, + it.priceAmountMicros, + it.priceCurrencyCode, + it.billingPeriod, + it.billingCycleCount + ) + } + } internal fun purchase( - product: CBProduct, + purchaseProductParams: PurchaseProductParams, purchaseCallBack: CBCallback.PurchaseCallback ) { this.purchaseCallBack = purchaseCallBack onConnected({ status -> - if (status) - purchase(product) - else + if (status) { + purchase(purchaseProductParams) + } else purchaseCallBack.onError( connectionError ) @@ -146,36 +191,66 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { }) } + /* Purchase the product: Initiates the billing flow for an In-app-purchase */ - private fun purchase(product: CBProduct) { - this.product = product - val skuDetails = product.skuDetails - - val params = BillingFlowParams.newBuilder() - .setSkuDetails(skuDetails) - .build() - - billingClient?.launchBillingFlow(mContext as Activity, params) - .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 (product.skuDetails.type == ProductType.SUBS.value) { - purchaseCallBack?.onError( - billingError - ) + private fun purchase(purchaseProductParams: PurchaseProductParams) { + this.purchaseProductParams = purchaseProductParams + val offerToken = purchaseProductParams.offerToken + + val queryProductDetails = arrayListOf(QueryProductDetailsParams.Product.newBuilder() + .setProductId(purchaseProductParams.product.id) + .setProductType(purchaseProductParams.product.type.value) + .build()) + + val productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(queryProductDetails).build() + + billingClient?.queryProductDetailsAsync( + productDetailsParams + ) { billingResult, productsDetail -> + if (billingResult.responseCode == OK && productsDetail != null) { + val productDetailsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productsDetail.first()) + offerToken?.let { productDetailsBuilder.setOfferToken(it) } + val productDetailsParamsList = + listOf(productDetailsBuilder.build()) + + val billingFlowParams = + BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList) + .build() + + 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( - billingError - ) + oneTimePurchaseCallback?.onError(throwCBException(billingResult)) } + } + } + } /** @@ -202,6 +277,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { OK -> { return true } + FEATURE_NOT_SUPPORTED -> { return false } @@ -229,6 +305,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { Purchase.PurchaseState.PURCHASED -> { acknowledgePurchase(purchase) } + Purchase.PurchaseState.PENDING -> { purchaseCallBack?.onError( CBException( @@ -239,6 +316,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) ) } + Purchase.PurchaseState.UNSPECIFIED_STATE -> { purchaseCallBack?.onError( CBException( @@ -252,35 +330,37 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } } - else -> { - if (product.skuDetails.type == ProductType.SUBS.value) - purchaseCallBack?.onError( - throwCBException(billingResult) - ) - else - oneTimePurchaseCallback?.onError( - throwCBException(billingResult) - ) - } + + else -> { + if (purchaseProductParams.product.type == ProductType.SUBS) + purchaseCallBack?.onError( + throwCBException(billingResult) + ) + else + oneTimePurchaseCallback?.onError( + throwCBException(billingResult) + ) + } } } /* Acknowledge the Purchases */ private fun acknowledgePurchase(purchase: Purchase) { - when(product.productType){ - ProductType.SUBS -> { - isAcknowledgedPurchase(purchase,{ - validateReceipt(purchase.purchaseToken, product) + when (purchaseProductParams.product.type) { + ProductType.SUBS -> { + isAcknowledgedPurchase(purchase, { + validateReceipt(purchase.purchaseToken, purchaseProductParams.product) }, { purchaseCallBack?.onError(it) }) } + ProductType.INAPP -> { if (CBPurchase.productType == OneTimeProductType.CONSUMABLE) { consumeAsyncPurchase(purchase.purchaseToken) } else { isAcknowledgedPurchase(purchase, { - validateNonSubscriptionReceipt(purchase.purchaseToken, product) + validateNonSubscriptionReceipt(purchase.purchaseToken, purchaseProductParams.product) }, { oneTimePurchaseCallback?.onError(it) }) @@ -289,7 +369,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } } - private fun isAcknowledgedPurchase(purchase: Purchase, success: () -> Unit, error: (CBException) -> Unit){ + private fun isAcknowledgedPurchase( + purchase: Purchase, + success: () -> Unit, + error: (CBException) -> Unit + ) { if (!purchase.isAcknowledged) { val params = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) @@ -312,6 +396,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) } } + else -> { error( throwCBException(billingResult) @@ -325,10 +410,11 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { /* Consume the Purchases */ private fun consumeAsyncPurchase(token: String) { consumePurchase(token) { billingResult, purchaseToken -> - when(billingResult.responseCode){ + when (billingResult.responseCode) { OK -> { - validateNonSubscriptionReceipt(purchaseToken, product) + validateNonSubscriptionReceipt(purchaseToken, purchaseProductParams.product) } + else -> { oneTimePurchaseCallback?.onError( throwCBException(billingResult) @@ -379,6 +465,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { purchaseCallBack?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) } } + is ChargebeeResult.Error -> { Log.e(TAG, "Exception from Server - validateReceipt() : ${it.exp.message}") purchaseCallBack?.onError(it.exp) @@ -459,7 +546,8 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { private fun queryPurchaseHistoryAsync( productType: String, purchaseTransactionList: (List?) -> Unit ) { - billingClient?.queryPurchaseHistoryAsync(productType) { billingResult, subsHistoryList -> + val queryPurchaseHistoryParams = QueryPurchaseHistoryParams.newBuilder().setProductType(productType).build() + billingClient?.queryPurchaseHistoryAsync(queryPurchaseHistoryParams) { billingResult, subsHistoryList -> if (billingResult.responseCode == OK) { val purchaseHistoryList = subsHistoryList?.map { it.toPurchaseTransaction(productType) @@ -517,6 +605,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { Log.i(TAG, "Google Billing Setup Done!") status(true) } + else -> { connectionError(throwCBException(billingResult)) } @@ -533,7 +622,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { if (status) queryPurchaseHistory { purchaseHistoryList -> val purchaseTransaction = purchaseHistoryList.filter { - it.productId.first() == product.productId + it.productId.first() == product.id } val transaction = purchaseTransaction.firstOrNull() transaction?.let { @@ -557,12 +646,12 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { ) { this.oneTimePurchaseCallback = oneTimePurchaseCallback onConnected({ status -> - if (status) - purchase(product) - else - oneTimePurchaseCallback.onError( - connectionError - ) + if (status) { + val purchaseParams = PurchaseProductParams(product) + purchase(purchaseParams) + } else { + oneTimePurchaseCallback.onError(connectionError) + } }, { error -> oneTimePurchaseCallback.onError(error) }) @@ -590,8 +679,12 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { oneTimePurchaseCallback?.onError(CBException(ErrorDetail(message = GPErrorCode.PurchaseInvalid.errorMsg))) } } + is ChargebeeResult.Error -> { - Log.e(TAG, "Exception from Server - validateNonSubscriptionReceipt() : ${it.exp.message}") + Log.e( + TAG, + "Exception from Server - validateNonSubscriptionReceipt() : ${it.exp.message}" + ) oneTimePurchaseCallback?.onError(it.exp) } } @@ -607,7 +700,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { if (status) queryPurchaseHistory { purchaseHistoryList -> val purchaseTransaction = purchaseHistoryList.filter { - it.productId.first() == product.productId + it.productId.first() == product.id } val transaction = purchaseTransaction.firstOrNull() transaction?.let { @@ -615,7 +708,6 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener { } ?: run { completionCallback.onError(itemNotOwnedException()) } - } else completionCallback.onError( connectionError diff --git a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt index 34e871f..079a2e0 100644 --- a/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt +++ b/chargebee/src/main/java/com/chargebee/android/billingservice/CBPurchase.kt @@ -25,7 +25,7 @@ object CBPurchase { * Get the product ID's from chargebee system */ @JvmStatic - fun retrieveProductIdentifers( + fun retrieveProductIdentifiers( params: Array = arrayOf(), completion: (CBProductIDResult>) -> Unit ) { @@ -54,41 +54,24 @@ object CBPurchase { } /** - * Buy the product with/without customer id - * @param [product] The product that wish to purchase - * @param [callback] listener will be called when product purchase completes. - */ - @Deprecated( - message = "This will be removed in upcoming release, Please use API fun - purchaseProduct(product: CBProduct, customer : CBCustomer? = null, callback)", - level = DeprecationLevel.WARNING - ) - @JvmStatic - fun purchaseProduct( - product: CBProduct, customerID: String, - callback: CBCallback.PurchaseCallback - ) { - customer = CBCustomer(customerID, "", "", "") - purchaseProduct(product, callback) - } - - /** - * Buy the product with/without customer data - * @param [product] The product that wish to purchase + * Buy Subscription product with/without customer data + * @param [purchaseProductParams] The purchase parameters of the product to be purchased. + * @param [customer] Optional. Customer Object. * @param [callback] listener will be called when product purchase completes. */ @JvmStatic fun purchaseProduct( - product: CBProduct, customer: CBCustomer? = null, + purchaseProductParams: PurchaseProductParams, customer: CBCustomer? = null, callback: CBCallback.PurchaseCallback ) { this.customer = customer - purchaseProduct(product, callback) + purchaseProduct(purchaseProductParams, callback) } - private fun purchaseProduct(product: CBProduct, callback: CBCallback.PurchaseCallback) { + private fun purchaseProduct(purchaseProductParams: PurchaseProductParams, callback: CBCallback.PurchaseCallback) { isSDKKeyValid({ - log(customer, product) - billingClientManager?.purchase(product, callback) + log(customer, purchaseProductParams.product.id) + billingClientManager?.purchase(purchaseProductParams, callback) }, { callback.onError(it) }) @@ -111,7 +94,7 @@ object CBPurchase { this.productType = productType isSDKKeyValid({ - log(CBPurchase.customer, product, productType) + log(CBPurchase.customer, product.id, productType) billingClientManager?.purchaseNonSubscriptionProduct(product, callback) }, { callback.onError(it) @@ -195,7 +178,7 @@ object CBPurchase { completion: (ChargebeeResult) -> Unit ) { try { - validateReceipt(purchaseToken, product.productId, completion) + validateReceipt(purchaseToken, product.id, completion) } catch (exp: Exception) { Log.e(javaClass.simpleName, "Exception in validateReceipt() :" + exp.message) ChargebeeResult.Error( @@ -257,7 +240,7 @@ object CBPurchase { product: CBProduct, completion: (ChargebeeResult) -> Unit ) { - validateNonSubscriptionReceipt(purchaseToken, product.productId, completion) + validateNonSubscriptionReceipt(purchaseToken, product.id, completion) } internal fun validateNonSubscriptionReceipt( @@ -384,8 +367,8 @@ object CBPurchase { return billingClientManager as BillingClientManager } - private fun log(customer: CBCustomer?, product: CBProduct, productType: OneTimeProductType? = null) { - val additionalInfo = additionalInfo(customer, product, productType) + private fun log(customer: CBCustomer?, productId: String, productType: OneTimeProductType? = null) { + val additionalInfo = additionalInfo(customer, productId, productType) val logger = CBLogger( name = "buy", action = "before_purchase_command", @@ -393,8 +376,8 @@ object CBPurchase { ) ResultHandler.safeExecute { logger.info() } } - private fun additionalInfo(customer: CBCustomer?, product: CBProduct, productType: OneTimeProductType? = null): Map { - val map = mutableMapOf("product" to product.productId) + private fun additionalInfo(customer: CBCustomer?, productId: String, productType: OneTimeProductType? = null): Map { + val map = mutableMapOf("product" to productId) customer?.let { map["customerId"] = (it.id ?: "") } productType?.let { map["productType"] = it.toString() } return map diff --git a/chargebee/src/main/java/com/chargebee/android/models/Products.kt b/chargebee/src/main/java/com/chargebee/android/models/Products.kt index 88e63a8..a915bf6 100644 --- a/chargebee/src/main/java/com/chargebee/android/models/Products.kt +++ b/chargebee/src/main/java/com/chargebee/android/models/Products.kt @@ -1,13 +1,26 @@ package com.chargebee.android.models -import com.android.billingclient.api.SkuDetails import com.chargebee.android.billingservice.ProductType data class CBProduct( - val productId: String, - val productTitle: String, - val productPrice: String, - var skuDetails: SkuDetails, - var subStatus: Boolean, - var productType: ProductType -) \ No newline at end of file + val id: String, + val title: String, + val description: String, + val type: ProductType, + val subscriptionOffers: List?, + val oneTimePurchaseOffer: PricingPhase?, +) + +data class SubscriptionOffer( + val basePlanId: String, + val offerId: String?, + val offerToken: String, + val pricingPhases: List +) +data class PricingPhase( + val formattedPrice: String, + val amountInMicros: Long, + val currencyCode: String, + val billingPeriod: String? = null, + val billingCycleCount: Int? = null +) diff --git a/chargebee/src/main/java/com/chargebee/android/models/PurchaseProductParams.kt b/chargebee/src/main/java/com/chargebee/android/models/PurchaseProductParams.kt new file mode 100644 index 0000000..1d4dd71 --- /dev/null +++ b/chargebee/src/main/java/com/chargebee/android/models/PurchaseProductParams.kt @@ -0,0 +1,6 @@ +package com.chargebee.android.models + +data class PurchaseProductParams( + val product: CBProduct, + val offerToken: String? = null +) \ No newline at end of file diff --git a/chargebee/src/test/java/com/android/billingclient/api/StubProductDetails.kt b/chargebee/src/test/java/com/android/billingclient/api/StubProductDetails.kt new file mode 100644 index 0000000..9bff30c --- /dev/null +++ b/chargebee/src/test/java/com/android/billingclient/api/StubProductDetails.kt @@ -0,0 +1,38 @@ +package com.android.billingclient.api + +import kotlin.reflect.KClass + +fun KClass.create(): ProductDetails { + val productDetails = ProductDetails("{\n" + + "\t\"productId\": \"gold\",\n" + + "\t\"type\": \"subs\",\n" + + "\t\"title\": \"Gold Plan (com.chargebee.newsample (unreviewed))\",\n" + + "\t\"name\": \"Gold Plan\",\n" + + "\t\"localizedIn\": [\"en-US\"],\n" + + "\t\"skuDetailsToken\": \"AEuhp4Ln65Xw7Do9yIO-o4XIj7rAvn_FD90WWajD79kzt0GiNuNm2ACU15T3q56qZTs=\",\n" + + "\t\"subscriptionOfferDetails\": [{\n" + + "\t\t\"offerIdToken\": \"AUj\\/YhiCrZ\\/NvaFihCC8rjAWfXEsLtf\\/qPgutEo1M04GIc8psPY06GcWBpun6qf\\/NhMXcQe3KmD+rbgud2XiLO3ptF41\\/HWcHR7YfYcU7brJ6mM=\",\n" + + "\t\t\"basePlanId\": \"weekly\",\n" + + "\t\t\"pricingPhases\": [{\n" + + "\t\t\t\"priceAmountMicros\": 20000000,\n" + + "\t\t\t\"priceCurrencyCode\": \"INR\",\n" + + "\t\t\t\"formattedPrice\": \"₹20.00\",\n" + + "\t\t\t\"billingPeriod\": \"P1W\",\n" + + "\t\t\t\"recurrenceMode\": 1\n" + + "\t\t}],\n" + + "\t\t\"offerTags\": []\n" + + "\t}, {\n" + + "\t\t\"offerIdToken\": \"AUj\\/YhgOwTW\\/BAGR2Po8uAsNJc6G+Z5xSDRBnDU7VJ5GN21yhMvuUjUMFDNCwEu+GtDaN2CzYoLqu7wHu\\/T+37S1KlyLFi0tfSAZcJE5MisuY+hKUuRJ\",\n" + + "\t\t\"basePlanId\": \"monthly\",\n" + + "\t\t\"pricingPhases\": [{\n" + + "\t\t\t\"priceAmountMicros\": 40000000,\n" + + "\t\t\t\"priceCurrencyCode\": \"INR\",\n" + + "\t\t\t\"formattedPrice\": \"₹40.00\",\n" + + "\t\t\t\"billingPeriod\": \"P1M\",\n" + + "\t\t\t\"recurrenceMode\": 1\n" + + "\t\t}],\n" + + "\t\t\"offerTags\": []\n" + + "\t}]\n" + + "}") + return productDetails +} \ No newline at end of file diff --git a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt index 1511cbe..700f81b 100644 --- a/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/billingservice/BillingClientManagerTest.kt @@ -10,9 +10,12 @@ import com.chargebee.android.billingservice.CBCallback.ListProductsCallback import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.CBProductIDResult import com.chargebee.android.exceptions.ChargebeeResult +import com.chargebee.android.fixtures.otpProducts +import com.chargebee.android.fixtures.subProducts import com.chargebee.android.models.CBNonSubscriptionResponse import com.chargebee.android.models.CBProduct import com.chargebee.android.models.NonSubscription +import com.chargebee.android.models.PurchaseProductParams import com.chargebee.android.network.* import com.chargebee.android.resources.CatalogVersion import com.chargebee.android.resources.ReceiptResource @@ -61,8 +64,8 @@ class BillingClientManagerTest { private val receiptDetail = ReceiptDetail("subscriptionId", "customerId", "planId") private var callBackOneTimePurchase: CBCallback.OneTimePurchaseCallback? = null private val nonSubscriptionDetail = NonSubscription("invoiceId", "customerId", "chargeId") - private val otpProducts = CBProduct("test.consumable","Example product","100.0", SkuDetails(""),true, productType = ProductType.INAPP) - private val subProducts = CBProduct("chargebee.premium.android","Premium Plan","", SkuDetails(""),true, ProductType.SUBS) + private val productDetails = ProductDetails::class.create() + @Before fun setUp() { @@ -150,7 +153,7 @@ class BillingClientManagerTest { val IDs = java.util.ArrayList() IDs.add("") CoroutineScope(Dispatchers.IO).launch { - Mockito.`when`(CBPurchase.retrieveProductIdentifers(queryParam) { + Mockito.`when`(CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { assertThat(it,instanceOf(CBProductIDResult::class.java)) @@ -234,7 +237,7 @@ class BillingClientManagerTest { val productsIds = java.util.ArrayList() productsIds.add("") CoroutineScope(Dispatchers.IO).launch { - Mockito.`when`(CBPurchase.retrieveProductIdentifers(queryParam) { + Mockito.`when`(CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { assertThat(it,instanceOf(CBProductIDResult::class.java)) @@ -255,8 +258,9 @@ class BillingClientManagerTest { val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,"", + purchaseProductParams,null, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -270,10 +274,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(subProducts, it) + billingClientManager?.purchase(purchaseProductParams, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(purchaseProductParams,purchaseCallBack = it) } } lock.await() @@ -284,8 +288,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,"", + purchaseProductParams,null, object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { @@ -363,8 +368,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,customer, + purchaseProductParams,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -378,10 +384,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(subProducts, it) + billingClientManager?.purchase(purchaseProductParams, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(purchaseProductParams,purchaseCallBack = it) } } lock.await() @@ -394,8 +400,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) val lock = CountDownLatch(1) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,customer, + purchaseProductParams,customer, object : CBCallback.PurchaseCallback { override fun onError(error: CBException) { lock.countDown() @@ -409,10 +416,10 @@ class BillingClientManagerTest { }) Mockito.`when`(callBackPurchase?.let { - billingClientManager?.purchase(subProducts, it) + billingClientManager?.purchase(purchaseProductParams, it) }).thenReturn(Unit) callBackPurchase?.let { - verify(billingClientManager, times(1))?.purchase(subProducts,purchaseCallBack = it) + verify(billingClientManager, times(1))?.purchase(purchaseProductParams,purchaseCallBack = it) } } lock.await() @@ -423,8 +430,9 @@ class BillingClientManagerTest { val skuDetails = SkuDetails(jsonDetails) CoroutineScope(Dispatchers.IO).launch { + val purchaseProductParams = PurchaseProductParams(subProducts) CBPurchase.purchaseProduct( - subProducts,customer, + purchaseProductParams,customer, object : CBCallback.PurchaseCallback { override fun onSuccess(result: ReceiptDetail, status: Boolean) { diff --git a/chargebee/src/test/java/com/chargebee/android/fixtures/ProductFixtures.kt b/chargebee/src/test/java/com/chargebee/android/fixtures/ProductFixtures.kt new file mode 100644 index 0000000..f25002d --- /dev/null +++ b/chargebee/src/test/java/com/chargebee/android/fixtures/ProductFixtures.kt @@ -0,0 +1,15 @@ +package com.chargebee.android.fixtures + +import com.chargebee.android.billingservice.ProductType +import com.chargebee.android.models.CBProduct +import com.chargebee.android.models.PricingPhase +import com.chargebee.android.models.SubscriptionOffer + +val subsPricingPhase: PricingPhase = PricingPhase(formattedPrice = "1100.0 INR", amountInMicros = 1100000, currencyCode = "INR") +val subscriptionOffers: List = arrayListOf(SubscriptionOffer("basePlanId", "offerId", "offerToken", arrayListOf(subsPricingPhase))) +val oneTimePurchaseOffer: PricingPhase = PricingPhase(formattedPrice = "100.0 INR", amountInMicros = 100000, currencyCode = "INR") +val otpProducts = CBProduct("test.consumable","Example product", + "Description",ProductType.INAPP,null, oneTimePurchaseOffer) +val subProducts = CBProduct("chargebee.premium.android","Premium Plan", + "Description",ProductType.SUBS, subscriptionOffers, null) + diff --git a/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt b/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt index fff77f4..a987277 100644 --- a/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/resources/ItemResourceTest.kt @@ -32,7 +32,9 @@ class ItemResourceTest { site = "cb-imay-test", publishableApiKey = "test_EojsGoGFeHoc3VpGPQDOZGAxYy3d0FF3", sdkKey = "cb-j53yhbfmtfhfhkmhow3ramecom" - ) + ) { + + } } @After diff --git a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt index 83c71f9..38a6c60 100644 --- a/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt +++ b/chargebee/src/test/java/com/chargebee/android/restore/RestorePurchaseTest.kt @@ -4,8 +4,6 @@ import com.android.billingclient.api.* import com.chargebee.android.Chargebee import com.chargebee.android.ErrorDetail import com.chargebee.android.billingservice.CBCallback.RestorePurchaseCallback -import com.chargebee.android.billingservice.OneTimeProductType -import com.chargebee.android.billingservice.ProductType import com.chargebee.android.exceptions.CBException import com.chargebee.android.exceptions.ChargebeeResult import com.chargebee.android.models.* @@ -21,6 +19,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.junit.MockitoJUnitRunner @@ -72,6 +71,9 @@ class RestorePurchaseTest { CBRestorePurchaseManager.fetchStoreSubscriptionStatus( purchaseTransaction, + purchaseTransaction, + purchaseTransaction, + arrayListOf(), completionCallback = object : RestorePurchaseCallback { override fun onSuccess(result: List) { lock.countDown() @@ -109,7 +111,7 @@ class RestorePurchaseTest { Matchers.instanceOf(CBException::class.java) ) Mockito.verify(CBRestorePurchaseManager, Mockito.times(1)) - .getRestorePurchases(purchaseTransaction) + .getRestorePurchases(any(), any(), any(), any()) }) } lock.await()