Skip to content

Commit

Permalink
Merge pull request #61 from cb-amutha/feature/restore_purchase
Browse files Browse the repository at this point in the history
[Feature] Restore Purchases
  • Loading branch information
cb-amutha authored May 19, 2023
2 parents a217bde + 33f578e commit a57be57
Show file tree
Hide file tree
Showing 20 changed files with 423 additions and 29 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.0.12
New Feature
* Added restore purchases (#61)
## 0.0.11
SDK Improvements
* Package `utils` renamed to `models` and Added sdk method `retrieveProductIdentifiers` instead of `retrieveProductIdentifers` (#52)
Expand Down
51 changes: 40 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ To use Chargebee SDK in your Flutter app, follow these steps:

1. Add Chargebee as a dependency in your [pubspec.yaml](https://flutter.io/platform-plugins/ "https://flutter.io/platform-plugins/")file.

```dart
``` dart
dependencies:
chargebee_flutter: ^0.0.11
chargebee_flutter: ^0.0.12
```

2. Install dependency.

```dart
``` dart
flutter pub get
```

Expand All @@ -51,7 +51,7 @@ Before configuring the Chargebee Flutter SDK for syncing In-App Purchases, follo

Initialize the Chargebee Flutter SDK with your Chargebee site, Publishable API Key, and SDK Keys by including the following snippets in your app delegate during app startup.

```dart
``` dart
import 'package:chargebee_flutter/chargebee_flutter.dart';
try {
await Chargebee.configure("SITE_NAME", "API-KEY", "iOS SDK Key", "Android SDK Key");
Expand All @@ -68,7 +68,7 @@ This section describes how to use the SDK to integrate In-App Purchase informati

Every In-App Purchase subscription product that you configure in your account, can be configured in Chargebee as a Plan. Start by retrieving the IAP Product IDs from your Chargebee account using the following function.

```dart
``` dart
try {
final result = await Chargebee.retrieveProductIdentifiers(queryparam);
} on PlatformException catch (e) {
Expand All @@ -81,7 +81,7 @@ For example, query parameters can be passed as **"limit": "100"**.

Retrieve the IAP Product objects with Product IDs using the following function.

```dart
``` dart
try {
List<Product> products = await Chargebee.retrieveProducts({productList: "[Product ID's from Google or Apple]"});
} on PlatformException catch (e) {
Expand All @@ -97,7 +97,7 @@ Pass the product and customer identifier to the following function when your cus
`customerId` - **Optional parameter**. Although this is an optional parameter, we recommend passing customerId if it is available before user subscribes on your App. Passing this parameter ensures that customerId in your database matches with the customerId in Chargebee.
In case this parameter is not passed, then the **customerId** will be the same as the **SubscriptionId** created in Chargebee.

```dart
``` dart
try {
final result = await Chargebee.purchaseProduct(product, customerId);
print("subscription id : ${result.subscriptionId}");
Expand All @@ -109,13 +109,42 @@ 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.

#### 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.

To retrieve **inactive** purchases along with the **active** purchases for your app user, you can call the `restorePurchases()` function with the `includeInactivePurchases` parameter set to true. If you only want to restore active subscriptions, set the parameter to false. Here is an example of how to use the restorePurchases() function in your code with the `includeInactivePurchases` parameter set to true.

``` dart
try {
final result = await Chargebee.restorePurchases(true);
print("result : $result");
} on PlatformException catch (e) {
print('Error Message: ${e.message}, Error Details: ${e.details}, Error Code: ${e.code}');
}
```

##### Error Handling
In the event of any failures while finding associated subscriptions for the restored items, The SDK will return an error, as mentioned in the following table.

These are the possible error codes and their descriptions:
| Error Code | Description |
|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| 2014 | This error occurs when the user attempts to restore a purchase, but there is no receipt associated with the purchase. |
| 2015 | This error occurs when the attempt to refresh the receipt for a purchase fails. |
| 2016 | This error occurs when the attempt to restore a purchase fails for reasons other than a missing or invalid receipt. |
| 2017 | This error occurs when the URL for the receipt bundle provided during the restore process is invalid or cannot be accessed. |
| 2018 | This error occurs when the data contained within the receipt is not valid or cannot be parsed. |
| 2019 | This error occurs when there are no products available to restore. |
| 2020 | This error occurs when there is an error with the Chargebee service during the restore process.

#### 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.

Use query parameters - Subscription ID, Customer ID, or Status for checking the Subscription status on Chargebee and confirm the access - granted or denied.

```dart
``` dart
try {
final result = await Chargebee.retrieveSubscriptions(queryparam);
} on PlatformException catch (e) {
Expand All @@ -129,7 +158,7 @@ For example, query parameters can be passed as **"customer_id" : "id"**, **"subs

Use the query parameter - Subscription ID for retrieving the list of [entitlements](https://www.chargebee.com/docs/2.0/entitlements.html) associated with the subscription.

```dart
``` dart
try {
final result = await Chargebee.retrieveEntitlements(queryparam);
} on PlatformException catch (e) {
Expand All @@ -144,7 +173,7 @@ For example, query parameters can be passed as **"subscriptionId": "id"**.

If your Chargebee site is configured to Product Catalog 2.0, use the following functions to retrieve the item list.

```dart
``` dart
try {
final result = await Chargebee.retrieveAllItems(queryparam);
} on PlatformException catch (e) {
Expand All @@ -157,7 +186,7 @@ For example, query parameters can be passed as **"sort_by[desc]" : "name"** or *

If your Chargebee site is configured to Product Catalog 1.0, use the relevant functions to retrieve the plan list.

```dart
``` dart
try {
final result = await Chargebee.retrieveAllPlans(queryparam);
} on PlatformException catch (e) {
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.11'
version '0.0.12'

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.16'
implementation 'com.chargebee:chargebee-android:1.0.17'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.android.billingclient:billing-ktx:4.0.0'
}
38 changes: 38 additions & 0 deletions android/src/main/kotlin/com/chargebee/flutter/sdk/CBNativeError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.chargebee.flutter.sdk

import com.android.billingclient.api.BillingClient

enum class CBNativeError(val code: Int) {
// Restore Error
SERVICE_TIMEOUT(2014),
FEATURE_NOT_SUPPORTED(2015),
SERVICE_UNAVAILABLE(2016),
DEVELOPER_ERROR(2017),
ERROR(2018),
SERVICE_DISCONNECTED(2019),
USER_CANCELED(2020),
BILLING_UNAVAILABLE(2021),
ITEM_UNAVAILABLE(2022),
ITEM_NOT_OWNED(2023),
ITEM_ALREADY_OWNED(2024),
UNKNOWN(0);

companion object {
fun billingResponseCode(code: Int): CBNativeError {
return when (code) {
BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> SERVICE_TIMEOUT
BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> FEATURE_NOT_SUPPORTED
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> SERVICE_UNAVAILABLE
BillingClient.BillingResponseCode.DEVELOPER_ERROR -> DEVELOPER_ERROR
BillingClient.BillingResponseCode.ERROR -> ERROR
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> SERVICE_DISCONNECTED
BillingClient.BillingResponseCode.USER_CANCELED -> USER_CANCELED
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> BILLING_UNAVAILABLE
BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> ITEM_UNAVAILABLE
BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> ITEM_NOT_OWNED
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> ITEM_ALREADY_OWNED
else -> UNKNOWN
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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
Expand All @@ -14,7 +16,6 @@ import com.chargebee.android.exceptions.CBException
import com.chargebee.android.exceptions.CBProductIDResult
import com.chargebee.android.exceptions.ChargebeeResult
import com.chargebee.android.models.*
import com.chargebee.android.models.CBProduct.*
import com.chargebee.android.network.ReceiptDetail
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
Expand Down Expand Up @@ -88,6 +89,10 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
retrieveEntitlements(params, result)
}
}
"restorePurchases" -> {
val params = call.arguments() as? Map<String, Boolean>?
restorePurchases(result, params)
}
else -> {
Log.d(javaClass.simpleName, "Implementation not Found")
result.notImplemented()
Expand Down Expand Up @@ -141,6 +146,25 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
})
}

private fun restorePurchases(resultCallback: Result, queryParams: Map<String, Boolean>?) {
val includeInactivePurchases = queryParams?.get("includeInactivePurchases") as Boolean
CBPurchase.restorePurchases(
activity,
includeInactivePurchases,
object : CBCallback.RestorePurchaseCallback {
override fun onSuccess(result: List<CBRestoreSubscription>) {
val restoreSubscription = result.map { subscription ->
Gson().toJson(subscription.toMap())
}
resultCallback.success(restoreSubscription)
}

override fun onError(error: CBException) {
onStoreError(error, resultCallback)
}
})
}

private fun purchaseProduct(args: Map<String, Any>, result: Result) {
val customerID = args["customerId"] as String
val arrayList: ArrayList<String> = ArrayList<String>()
Expand Down Expand Up @@ -321,19 +345,27 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
}

private fun onError(error: CBException, result: Result) {
error("${error.httpStatusCode}", error, result)
}

private fun onStoreError(error: CBException, result: Result) {
val errorCode = error.httpStatusCode?.let { CBNativeError.billingResponseCode(it).code }
error("$errorCode", error, result)
}

private fun error(errorCode: String, error: CBException, result: Result) {
try {
result.error(
"${error.httpStatusCode}", "${
errorCode, "${
Gson().fromJson(
error.message,
ErrorDetail::class.java
).message
}", error.localizedMessage
)
} catch (exp: Exception) {
result.error("${error.httpStatusCode}", "${error.message}", error.localizedMessage)
result.error(errorCode, "${error.message}", error.localizedMessage)
}

}
}

Expand Down Expand Up @@ -371,3 +403,10 @@ fun CBProduct.periodUnit(): String {
}
}

internal fun CBRestoreSubscription.toMap(): Map<String, String> {
return mapOf(
"subscriptionId" to subscriptionId,
"planId" to planId,
"storeStatus" to storeStatus,
)
}
14 changes: 14 additions & 0 deletions example/integration_test/chargebee_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ void main() {
await chargebeeTest.retrieveEntitlements();
await chargebeeTest.retrieveAllItems();
await chargebeeTest.retrieveAllPlans();
await chargebeeTest.restorePurchases(true);
});
});
}
Expand Down Expand Up @@ -200,4 +201,17 @@ class ChargebeeTest {
fail('Error: ${e.message}');
}
}

Future<void> restorePurchases(bool includeInActivePurchase) async {
tester.printToConsole('Start restore purchases and sync with Chargebee');
try {
final subscriptions = await Chargebee.restorePurchases(includeInActivePurchase);
debugPrint('Purchases : $subscriptions');
expect(subscriptions.isNotEmpty, true);
tester.printToConsole('Purchases restored successfully!');
} on PlatformException catch (e) {
fail('Error: ${e.message}');
}
}

}
33 changes: 33 additions & 0 deletions example/ios/StoreKit Configuration.storekit
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"identifier" : "393F510B",
"nonRenewingSubscriptions" : [
{
"displayPrice" : "0.99",
"familyShareable" : false,
"internalID" : "0F260BA9",
"localizations" : [
{
"description" : "",
"displayName" : "",
"locale" : "en_US"
}
],
"productID" : "com.temporary.id",
"referenceName" : "Testing",
"type" : "NonRenewingSubscription"
}
],
"products" : [

],
"settings" : {

},
"subscriptionGroups" : [

],
"version" : {
"major" : 1,
"minor" : 1
}
}
4 changes: 3 additions & 1 deletion example/lib/Constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class Constants {
'Get Product Identifiers',
'Get Entitlements',
'Get Plans',
'Get Items'
'Get Items',
'Restore Purchase'
];

static const String config = 'Configure';
Expand All @@ -20,4 +21,5 @@ class Constants {
static const String getEntitlements= 'Get Entitlements';
static const String getAllPlans = 'Get Plans';
static const String getAllItems = 'Get Items';
static const String restorePurchase = 'Restore Purchase';
}
Loading

0 comments on commit a57be57

Please sign in to comment.