Skip to content

Commit

Permalink
Merge pull request #64 from cb-amutha/feature/otp_support
Browse files Browse the repository at this point in the history
One Time Purchase Support
  • Loading branch information
cb-amutha authored Jul 26, 2023
2 parents 61b6ad8 + 8e0a1a6 commit 6de3236
Show file tree
Hide file tree
Showing 15 changed files with 1,059 additions and 278 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.0.14
New Feature
* Adds one time purchase support. (#64)
* Use `Chargebee.purchaseNonSubscriptionProduct` to purchase one time purchase product on Apple App Store and Google Play Store.
* Use `Chargebee.validateReceiptForNonSubscriptions` to validate one time purchase receipt if syncing failed with Chargebee after the successful purchase on Apple App Store and Google
Play Store.
## 0.0.13
SDK Improvements
* Added cache retry mechanism for validating the receipt. (#62)
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ try {

The above function will handle the purchase against Apple App Store or Google Play Store and send the in-app purchase receipt for server-side receipt 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 Apple App Store and Google Play Store and then 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. The Apple App Store supports three types of one-time purchases `consumable`, `non_consumable` and `non_renewing_subscription`. The Google Play Store supports two types of one-time purchases `consumable` and `non_consumable`.

``` dart
try {
final productType = OneTimeProductType.consumable;
final customer = CBCustomer('id','','','');
final result = await Chargebee.purchaseNonSubscriptionProduct(product, productType, customer);
debugPrint('invoice id : ${result.invoiceId}');
debugPrint('charge id : ${result.chargeId}');
debugPrint('customer id : ${result.customerId}');
} on PlatformException catch (e) {
print('Error Message: ${e.message}, Error Details: ${e.details}, Error Code: ${e.code}');
}
```

The given code defines a function named `purchaseNonSubscriptionProduct` in the Chargebee class, which takes three input parameters:

- `product`: An instance of `Product` class, representing the product to be purchased from the Apple App Store or 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`, or `non_renewing_subscription`. Currently `non_renewing_subscription` product type supports only in Apple App Store.

The function is called asynchronously, and it returns a `Result` object with a `success` or `failure` case, as mentioned are below.
- If the purchase is successful, it returns `NonSubscriptionPurchaseResult` object. which includes the `invoiceId`, `chargeId`, and `customerId` associated with the purchase.
- If there is any failure during the purchase, it returns `PlatformException`. which includes an error object that can be used to handle the error.

#### Restore Purchases

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 @@ -150,7 +176,7 @@ Receipt validation is crucial to ensure that the purchases made by your users ar
* When the network connectivity is lost after the purchase is completed at Apple App Store/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 `productId` 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().

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

``` dart
try {
Expand All @@ -162,6 +188,21 @@ try {
}
```

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

``` dart
try {
final productType = OneTimeProductType.consumable;
final customer = CBCustomer('id','','','');
final result = await Chargebee.validateReceiptForNonSubscriptions(productId, productType, customer);
debugPrint('invoice id : ${result.invoiceId}');
debugPrint('charge id : ${result.chargeId}');
debugPrint('customer id : ${result.customerId}');
} on PlatformException catch (e) {
print('Error Message: ${e.message}, Error Details: ${e.details}, Error Code: ${e.code}');
}
```

#### Get Subscription Status for Existing Subscribers using Query Parameters

Use this method to check the subscription status of a subscriber who has already purchased the product.
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group 'com.chargebee.flutter.sdk'
version '0.0.13'
version '0.0.14'

buildscript {
ext.kotlin_version = '1.6.0'
Expand Down Expand Up @@ -47,7 +47,7 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.chargebee:chargebee-android:1.0.18'
implementation 'com.chargebee:chargebee-android:1.0.20'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.android.billingclient:billing-ktx:4.0.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import android.app.Activity
import android.content.Context
import android.util.Log
import androidx.annotation.NonNull
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED
import com.chargebee.android.Chargebee
import com.chargebee.android.ErrorDetail
import com.chargebee.android.billingservice.CBCallback
import com.chargebee.android.billingservice.CBPurchase
import com.chargebee.android.billingservice.GPErrorCode
import com.chargebee.android.billingservice.*
import com.chargebee.android.network.CBAuthResponse
import com.chargebee.android.exceptions.CBException
import com.chargebee.android.exceptions.CBProductIDResult
Expand Down Expand Up @@ -64,6 +60,11 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
purchaseProduct(args, result)
}
}
"purchaseNonSubscriptionProduct" -> {
if (args != null) {
purchaseNonSubscriptionProduct(args, result)
}
}
"retrieveSubscriptions" -> {
val params = call.arguments() as? Map<String, String>?
if (params != null) {
Expand Down Expand Up @@ -99,6 +100,11 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
validateReceipt(args, result)
}
}
"validateReceiptForNonSubscriptions" -> {
if (args != null) {
validateReceiptForNonSubscriptions(args, result)
}
}
else -> {
Log.d(javaClass.simpleName, "Implementation not Found")
result.notImplemented()
Expand Down Expand Up @@ -217,6 +223,53 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
})
}

private fun purchaseNonSubscriptionProduct(args: Map<String, Any>, callback: Result) {
val customer = CBCustomer(
args["customerId"] as String,
args["firstName"] as String,
args["lastName"] as String,
args["email"] as String
)
val type = args["product_type"] as String
val productType = if (type == OneTimeProductType.CONSUMABLE.value)
OneTimeProductType.CONSUMABLE
else
OneTimeProductType.NON_CONSUMABLE

val product = arrayListOf(args["product"] as String)
CBPurchase.retrieveProducts(
activity,
product,
object : CBCallback.ListProductsCallback<ArrayList<CBProduct>> {
override fun onSuccess(productIDs: ArrayList<CBProduct>) {
if (productIDs.size == 0) {
onError(
CBException(ErrorDetail(GPErrorCode.ProductUnavailable.errorMsg)),
callback
)
return
}
CBPurchase.purchaseNonSubscriptionProduct(
productIDs.first(),
customer,
productType,
object : CBCallback.OneTimePurchaseCallback {
override fun onSuccess(result: NonSubscription, status: Boolean) {
callback.success(result.toMap())
}

override fun onError(error: CBException) {
onError(error, callback)
}
})
}

override fun onError(error: CBException) {
onError(error, callback)
}
})
}

private fun onResultMap(
id: String, planId: String, customerId: String, status: String
): String {
Expand Down Expand Up @@ -373,6 +426,52 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
})
}

private fun validateReceiptForNonSubscriptions(args: Map<String, Any>, callback: Result) {
val customer = CBCustomer(
args["customerId"] as String,
args["firstName"] as String,
args["lastName"] as String,
args["email"] as String
)
val type = args["product_type"] as String
val productType = if (type == OneTimeProductType.CONSUMABLE.value)
OneTimeProductType.CONSUMABLE
else
OneTimeProductType.NON_CONSUMABLE

val product = arrayListOf(args["product"] as String)
CBPurchase.retrieveProducts(activity,
product,
object : CBCallback.ListProductsCallback<ArrayList<CBProduct>> {
override fun onSuccess(productIDs: ArrayList<CBProduct>) {
if (productIDs.size == 0) {
onError(
CBException(ErrorDetail(GPErrorCode.ProductUnavailable.errorMsg)),
callback
)
return
}
CBPurchase.validateReceiptForNonSubscriptions(context = activity,
product = productIDs.first(),
customer = customer,
productType = productType,
object : CBCallback.OneTimePurchaseCallback {
override fun onSuccess(result: NonSubscription, status: Boolean) {
callback.success(result.toMap())
}

override fun onError(error: CBException) {
onError(error, callback)
}
})
}

override fun onError(error: CBException) {
onError(error, callback)
}
})
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
if (channel != null) {
channel.setMethodCallHandler(null);
Expand Down Expand Up @@ -435,12 +534,20 @@ fun CBProduct.convertPriceAmountInMicros(): Double {
}

fun CBProduct.subscriptionPeriod(): Map<String, Any> {
val subscriptionPeriod = skuDetails.subscriptionPeriod
val numberOfUnits = subscriptionPeriod.substring(1, subscriptionPeriod.length - 1).toInt()
return mapOf(
"periodUnit" to periodUnit(),
"numberOfUnits" to numberOfUnits
)
val subscriptionPeriodMap = if (skuDetails.type == ProductType.SUBS.value) {
val subscriptionPeriod = skuDetails.subscriptionPeriod
val numberOfUnits = subscriptionPeriod.substring(1, subscriptionPeriod.length - 1).toInt()
mapOf(
"periodUnit" to periodUnit(),
"numberOfUnits" to numberOfUnits
)
} else {
mapOf(
"periodUnit" to "",
"numberOfUnits" to 0
)
}
return subscriptionPeriodMap
}

fun CBProduct.periodUnit(): String {
Expand All @@ -460,3 +567,12 @@ internal fun CBRestoreSubscription.toMap(): Map<String, String> {
"storeStatus" to storeStatus,
)
}

internal fun NonSubscription.toMap(): String {
val resultMap = mapOf(
"invoiceId" to invoiceId,
"chargeId" to chargeId,
"customerId" to customerId,
)
return Gson().toJson(resultMap)
}
Loading

0 comments on commit 6de3236

Please sign in to comment.