Skip to content

Commit

Permalink
Merge pull request #72 from cb-amutha/feature/otp_purchase_support
Browse files Browse the repository at this point in the history
One Time Purchase Support
  • Loading branch information
cb-amutha committed Jul 10, 2023
2 parents 3c21100 + 9d16342 commit 15eef33
Show file tree
Hide file tree
Showing 20 changed files with 827 additions and 174 deletions.
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,32 @@ CBPurchase.purchaseProduct(product=CBProduct, customer=CBCustomer, object : CBCa
```
The above function will handle the purchase against Google Play Store and send the IAP token for server-side token verification to your Chargebee account. Use the Subscription ID returned by the above function, to check for Subscription status on Chargebee and confirm the access - granted or denied.

### One-Time Purchases
The `purchaseNonSubscriptionProduct` function handles the one-time purchase against Google Play Store and sends the IAP receipt for server-side receipt verification to your Chargebee account. Post verification a Charge corresponding to this one-time purchase will be created in Chargebee. There are two types of one-time purchases `consumable` and `non_consumable`.

```kotlin
CBPurchase.purchaseNonSubscriptionProduct(product = CBProduct, customer = CBCustomer, productType = OneTimeProductType.CONSUMABLE, callback = object : CBCallback.OneTimePurchaseCallback{
override fun onSuccess(result: NonSubscription, status:Boolean) {
Log.i(TAG, "invoice ID: ${result.invoiceId}")
Log.i(TAG, "charge ID: ${result.chargeId}")
Log.i(TAG, "customer ID: ${result.customerId}")
}
override fun onError(error: CBException) {
Log.e(TAG, "Error: ${error.message}")
}
})
```
The given code defines a function named `purchaseNonSubscriptionProduct` in the CBPurchase class, which takes four input parameters:

- `product`: An instance of `CBProduct` class, initialized with a `SkuDetails` instance representing the product to be purchased from the Google Play Store.
- `customer`: Optional. An instance of `CBCustomer` class, initialized with the customer's details such as `customerId`, `firstName`, `lastName`, and `email`.
- `productType`: An enum instance of `productType` type, indicating the type of product to be purchased. It can be either .`consumable`, or `non_consumable`.
- `callback`: The `OneTimePurchaseCallback` listener will be invoked when product purchase completes.
The function is called asynchronously, and it returns a `Result` object with a `success` or `failure` case, which can be handled in the listener.
- If the purchase is successful, the listener will be called with the `success` case, it returns `NonSubscriptionResponse` object. which includes the `customerId`, `chargeId`, and `invoiceId` associated with the purchase.
- If there is any failure during the purchase, the listener will be called with the `error` case, it returns `CBException`. which includes an error object that can be used to handle the error.
### Restore Purchase
The `restorePurchases()` function helps to recover your app user's previous purchases without making them pay again. Sometimes, your app user may want to restore their previous purchases after switching to a new device or reinstalling your app. You can use the `restorePurchases()` function to allow your app user to easily restore their previous purchases.
Expand Down Expand Up @@ -182,10 +208,10 @@ Receipt validation is crucial to ensure that the purchases made by your users ar

* Add a network listener, as shown in the example project.
* Save the product identifier in the cache once the purchase is initiated and clear the cache once the purchase is successful.
* When the network connectivity is lost after the purchase is completed at Google Play Store but not synced with Chargebee, retrieve the product from the cache once the network connection is back and initiate validateReceipt() by passing activity `Context`, `CBProduct` and `CBCustomer(optional)` as input. This will validate the receipt and sync the purchase in Chargebee as a subscription. For subscriptions, use the function to validateReceipt().
* When the network connectivity is lost after the purchase is completed at Google Play Store but not synced with Chargebee, retrieve the product from the cache once the network connection is back and initiate `validateReceipt() / validateReceiptForNonSubscriptions()` by passing activity `Context`, `CBProduct` and `CBCustomer(optional)` as input. This will validate the receipt and sync the purchase in Chargebee as a subscription or one-time purchase. For subscriptions, use the function to `validateReceipt()`;for one-time purchases, use the function `validateReceiptForNonSubscriptions()`.

Use the function available for the retry mechanism.
##### Function for validating the receipt
##### Function for validating the Subscriptions receipt

```kotlin
CBPurchase.validateReceipt(context = current activity context, product = CBProduct, customer = CBCustomer, object : CBCallback.PurchaseCallback<String> {
Expand All @@ -200,6 +226,21 @@ CBPurchase.validateReceipt(context = current activity context, product = CBProdu
})
```

##### Function for validating the One-Time Purchases receipt

```kotlin
CBPurchase.validateReceiptForNonSubscriptions(context = current activity context, product = CBProduct, customer = CBCustomer, productType = OneTimeProductType.CONSUMABLE, object : CBCallback.OneTimePurchaseCallback {
override fun onSuccess(result: NonSubscription, status: Boolean) {
Log.i(TAG, "invoice ID: ${result.invoiceId}")
Log.i(TAG, "charge ID: ${result.chargeId}")
Log.i(TAG, "customer ID: ${result.customerId}")
}
override fun onError(error: CBException) {
Log.e(TAG, "Error: ${error.message}")
}
})
```

### Get Subscription Status for Existing Subscribers
The following are methods for checking the subscription status of a subscriber who already purchased the product.

Expand Down
44 changes: 37 additions & 7 deletions app/src/main/java/com/chargebee/example/ExampleApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import android.content.SharedPreferences
import android.util.Log
import com.chargebee.android.billingservice.CBCallback
import com.chargebee.android.billingservice.CBPurchase
import com.chargebee.android.billingservice.OneTimeProductType
import com.chargebee.android.billingservice.ProductType
import com.chargebee.android.exceptions.CBException
import com.chargebee.android.models.CBProduct
import com.chargebee.android.models.NonSubscription
import com.chargebee.android.network.CBCustomer
import com.chargebee.android.network.ReceiptDetail
import com.chargebee.example.util.NetworkUtil
Expand All @@ -17,6 +20,12 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener {
private lateinit var networkUtil: NetworkUtil
private lateinit var sharedPreference: SharedPreferences
lateinit var mContext: Context
private val customer = CBCustomer(
id = "sync_receipt_android",
firstName = "Test",
lastName = "Purchase",
email = "[email protected]"
)

override fun onCreate() {
super.onCreate()
Expand Down Expand Up @@ -46,7 +55,10 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener {
productIdList,
object : CBCallback.ListProductsCallback<ArrayList<CBProduct>> {
override fun onSuccess(productIDs: ArrayList<CBProduct>) {
validateReceipt(mContext, productIDs.first())
if (productIDs.first().productType == ProductType.SUBS)
validateReceipt(mContext, productIDs.first())
else
validateNonSubscriptionReceipt(mContext, productIDs.first())
}

override fun onError(error: CBException) {
Expand All @@ -56,12 +68,7 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener {
}

private fun validateReceipt(context: Context, product: CBProduct) {
val customer = CBCustomer(
id = "sync_receipt_android",
firstName = "Test",
lastName = "Purchase",
email = "[email protected]"
)

CBPurchase.validateReceipt(
context = context,
product = product,
Expand All @@ -82,4 +89,27 @@ class ExampleApplication : Application(), NetworkUtil.NetworkListener {
}
})
}

private fun validateNonSubscriptionReceipt(context: Context, product: CBProduct) {
CBPurchase.validateReceiptForNonSubscriptions(
context = context,
product = product,
customer = customer,
productType = OneTimeProductType.CONSUMABLE,
completionCallback = object : CBCallback.OneTimePurchaseCallback {
override fun onSuccess(result: NonSubscription, status: Boolean) {
// Clear the local cache once receipt validation success
val editor = sharedPreference.edit()
editor.clear().apply()
Log.i(javaClass.simpleName, "Subscription ID: ${result.invoiceId}")
Log.i(javaClass.simpleName, "Plan ID: ${result.chargeId}")
Log.i(javaClass.simpleName, "Customer ID: ${result.customerId}")
Log.i(javaClass.simpleName, "Status: $status")
}

override fun onError(error: CBException) {
Log.e(javaClass.simpleName, "Exception :$error")
}
})
}
}
39 changes: 10 additions & 29 deletions app/src/main/java/com/chargebee/example/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener {
Log.i(javaClass.simpleName, "Google play product identifiers: $it")
alertListProductId(it)
}

this.mBillingViewModel!!.restorePurchaseResult.observeForever {
hideProgressDialog()
if (it.isNotEmpty()) {
alertSuccess("${it.size} purchases restored successfully")
} else {
alertSuccess("Purchases not found to restore")
}
}
}

private fun setListAdapter() {
Expand Down Expand Up @@ -133,7 +142,7 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener {
getSubscriptionId()
}
CBMenu.RestorePurchase.value -> {
restorePurchases()
mBillingViewModel?.restorePurchases(this)
}
else -> {
Log.i(javaClass.simpleName, " Not implemented")
Expand Down Expand Up @@ -208,34 +217,6 @@ class MainActivity : BaseActivity(), ListItemsAdapter.ItemClickListener {
})
}

private fun restorePurchases() {
showProgressDialog()
CBPurchase.restorePurchases(
context = this, includeInActivePurchases = true,
completionCallback = object : CBCallback.RestorePurchaseCallback {
override fun onSuccess(result: List<CBRestoreSubscription>) {
hideProgressDialog()
result.forEach {
Log.i(javaClass.simpleName, "status : ${it.storeStatus}")
}
CoroutineScope(Dispatchers.Main).launch {
if (result.isNotEmpty())
alertSuccess("${result.size} purchases restored successfully")
else
alertSuccess("Purchases not found to restore")
}
}

override fun onError(error: CBException) {
hideProgressDialog()
Log.e(javaClass.simpleName, "error message: ${error.message}")
CoroutineScope(Dispatchers.Main).launch {
showDialog("${error.message}, ${error.httpStatusCode}")
}
}
})
}

private fun alertListProductId(list: Array<String>) {
val builder = AlertDialog.Builder(this)
builder.setTitle("Chargebee Product IDs")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.chargebee.android.ProgressBarListener;
import com.chargebee.android.billingservice.OneTimeProductType;
import com.chargebee.android.billingservice.ProductType;
import com.chargebee.android.models.CBProduct;
import com.chargebee.android.network.CBCustomer;
import com.chargebee.example.BaseActivity;
Expand All @@ -32,6 +33,7 @@ public class BillingActivity extends BaseActivity implements ProductListAdapter.
private static final String TAG = "BillingActivity";
private int position = 0;
CBCustomer cbCustomer;
private EditText inputProductType;

@Override
protected void onCreate(Bundle savedInstanceState) {
Expand Down Expand Up @@ -125,31 +127,62 @@ private void getCustomerID() {
EditText inputFirstName = dialog.findViewById(R.id.firstNameText);
EditText inputLastName = dialog.findViewById(R.id.lastNameText);
EditText inputEmail = dialog.findViewById(R.id.emailText);
inputProductType = dialog.findViewById(R.id.productTypeText);
if (isOneTimeProduct()) inputProductType.setVisibility(View.VISIBLE);
else inputProductType.setVisibility(View.GONE);

Button dialogButton = dialog.findViewById(R.id.btn_ok);
dialogButton.setText("Ok");
dialogButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showProgressDialog();
String customerId = input.getText().toString();
String firstName = inputFirstName.getText().toString();
String lastName = inputLastName.getText().toString();
String email = inputEmail.getText().toString();
cbCustomer = new CBCustomer(customerId,firstName,lastName,email);
dialogButton.setOnClickListener(view -> {
String customerId = input.getText().toString();
String firstName = inputFirstName.getText().toString();
String lastName = inputLastName.getText().toString();
String email = inputEmail.getText().toString();
String productType = inputProductType.getText().toString();
cbCustomer = new CBCustomer(customerId,firstName,lastName,email);
if (isOneTimeProduct()){
if (checkProductTypeFiled()) {
if (productType.trim().equalsIgnoreCase(OneTimeProductType.CONSUMABLE.getValue())) {
purchaseNonSubscriptionProduct(OneTimeProductType.CONSUMABLE);
} else if (productType.trim().equalsIgnoreCase(OneTimeProductType.NON_CONSUMABLE.getValue())) {
purchaseNonSubscriptionProduct(OneTimeProductType.NON_CONSUMABLE);
}
dialog.dismiss();
}
} else {
purchaseProduct(customerId);
//purchaseProduct();
// purchaseProduct();
dialog.dismiss();
}
});
dialog.show();
}

private void purchaseProduct(String customerId){
private boolean checkProductTypeFiled(){
if (inputProductType.getText().toString().length() == 0) {
inputProductType.setError("This field is required");
return false;
}
return true;
}

private boolean isOneTimeProduct(){
return productList.get(position).getProductType() == ProductType.INAPP;
}

private void purchaseProduct(String customerId) {
showProgressDialog();
this.billingViewModel.purchaseProduct(this, productList.get(position), customerId);
}
private void purchaseProduct(){
this.billingViewModel.purchaseProduct(this,productList.get(position), cbCustomer);

private void purchaseProduct() {
showProgressDialog();
this.billingViewModel.purchaseProduct(this, productList.get(position), cbCustomer);
}

private void purchaseNonSubscriptionProduct(OneTimeProductType productType) {
showProgressDialog();
this.billingViewModel.purchaseNonSubscriptionProduct(this, productList.get(position), cbCustomer, productType);
}

private void updateSubscribeStatus(){
Expand Down
Loading

0 comments on commit 15eef33

Please sign in to comment.