Skip to content

Commit

Permalink
Merge pull request #62 from cb-amutha/impro/cache_receipt_retry
Browse files Browse the repository at this point in the history
Implemented retry mechanism for validate receipt
  • Loading branch information
cb-amutha committed Jun 15, 2023
2 parents a57be57 + 6efca13 commit 5ce3c06
Show file tree
Hide file tree
Showing 22 changed files with 636 additions and 170 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.0.13
SDK Improvements
* Added cache retry mechanism for validating the receipt. (#62)
* Use `Chargebee.validateReceipt` to validate the receipt if syncing failed with Chargebee after the successful purchase on Apple App Store and Google
Play Store.
## 0.0.12
New Feature
* Added restore purchases (#61)
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ To use Chargebee SDK in your Flutter app, follow these steps:

``` dart
dependencies:
chargebee_flutter: ^0.0.12
chargebee_flutter: ^0.0.13
```

2. Install dependency.
Expand Down Expand Up @@ -138,6 +138,26 @@ These are the possible error codes and their descriptions:
| 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.

#### Synchronization of Apple App Store/Google Play Store Purchases with Chargebee through Receipt Validation
Receipt validation is crucial to ensure that the purchases made by your users are synced with Chargebee. In rare cases, when a purchase is made at the Apple App Store/Google Play Store, and the network connection goes off or the server not responding, the purchase details may not be updated in Chargebee. In such cases, you can use a retry mechanism by following these steps:

* 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 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

``` dart
try {
final result = await Chargebee.validateReceipt(productId);
print("subscription id : ${result.subscriptionId}");
print("subscription status : ${result.status}");
} 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.12'
version '0.0.13'

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.17'
implementation 'com.chargebee:chargebee-android:1.0.18'
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 @@ -16,6 +16,7 @@ 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.network.CBCustomer
import com.chargebee.android.network.ReceiptDetail
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
Expand Down Expand Up @@ -93,6 +94,11 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
val params = call.arguments() as? Map<String, Boolean>?
restorePurchases(result, params)
}
"validateReceipt" -> {
if (args != null) {
validateReceipt(args, result)
}
}
else -> {
Log.d(javaClass.simpleName, "Implementation not Found")
result.notImplemented()
Expand Down Expand Up @@ -189,19 +195,11 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
receiptDetail: ReceiptDetail,
status: Boolean
) {
Log.i(
javaClass.simpleName,
"Subscription ID: ${receiptDetail.subscription_id}"
)
Log.i(javaClass.simpleName, "Status: $status")
Log.i(
javaClass.simpleName,
"Plan ID: ${receiptDetail.plan_id}"
)
result.success(
onResultMap(
receiptDetail.subscription_id,
receiptDetail.plan_id,
receiptDetail.customer_id,
"$status"
)
)
Expand All @@ -219,10 +217,13 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
})
}

fun onResultMap(id: String, planId: String, status: String): String {
subscriptionStatus.put("subscriptionId", id)
subscriptionStatus.put("planId", planId)
subscriptionStatus.put("status", status)
private fun onResultMap(
id: String, planId: String, customerId: String, status: String
): String {
subscriptionStatus["subscriptionId"] = id
subscriptionStatus["planId"] = planId
subscriptionStatus["customerId"] = customerId
subscriptionStatus["status"] = status
return Gson().toJson(subscriptionStatus)
}

Expand Down Expand Up @@ -323,6 +324,55 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar
}
}

private fun validateReceipt(args: Map<String, Any>, result: Result) {
val customer = CBCustomer(
args["customerId"] as String,
args["firstName"] as String,
args["lastName"] as String,
args["email"] as String
)
val arrayList: ArrayList<String> = ArrayList<String>()
arrayList.add(args["product"] as String)
CBPurchase.retrieveProducts(activity,
arrayList,
object : CBCallback.ListProductsCallback<ArrayList<CBProduct>> {
override fun onSuccess(productIDs: ArrayList<CBProduct>) {
if (productIDs.size == 0) {
onError(
CBException(ErrorDetail(GPErrorCode.ProductUnavailable.errorMsg)),
result
)
return
}
CBPurchase.validateReceipt(context = activity,
product = productIDs.first(),
customer = customer,
completionCallback = object : CBCallback.PurchaseCallback<String> {
override fun onSuccess(
receiptDetail: ReceiptDetail, status: Boolean
) {
result.success(
onResultMap(
receiptDetail.subscription_id,
receiptDetail.plan_id,
receiptDetail.customer_id,
"$status"
)
)
}

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

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

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
if (channel != null) {
channel.setMethodCallHandler(null);
Expand Down
2 changes: 1 addition & 1 deletion example/integration_test/chargebee_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class ChargebeeTest {

Future<void> retrieveProductIdentifiersWithParam() async {
tester.printToConsole(
'Fetch store specific product Ids from chargebee with params');
'Fetch store specific product Ids from chargebee with params',);
try {
final list = await Chargebee.retrieveProductIdentifiers({'limit': '10'});
debugPrint('list: $list');
Expand Down
65 changes: 62 additions & 3 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import 'dart:async';
import 'dart:developer';

import 'package:chargebee_flutter/chargebee_flutter.dart';
import 'package:chargebee_flutter_sdk_example/Constants.dart';
import 'package:chargebee_flutter_sdk_example/alertDialog.dart';
import 'package:chargebee_flutter_sdk_example/items_listview.dart';
import 'package:chargebee_flutter_sdk_example/network_connectivity.dart';
import 'package:chargebee_flutter_sdk_example/product_ids_listview.dart';
import 'package:chargebee_flutter_sdk_example/product_listview.dart';
import 'package:chargebee_flutter_sdk_example/progress_bar.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(const MyApp());

Expand Down Expand Up @@ -69,17 +73,54 @@ class _MyHomePageState extends State<MyHomePage> {
'limit': '5',
'channel[is]': 'play_store'
}; // eg. query params for getAllPlans
Map _source = {ConnectivityResult.none: false};
final NetworkConnectivity _networkConnectivity = NetworkConnectivity.instance;
String string = '';

_MyHomePageState();

@override
void initState() {
// For both iOS and Android
authentication('your-site', 'publishable_api_key', 'iOS ResourceID/SDK Key',
'Android ResourceID/SDK Key',);
super.initState();
_networkConnectivity.initialise();
_networkConnectivity.myStream.listen((source) {
_source = source;
switch (_source.keys.toList()[0]) {
case ConnectivityResult.mobile:
string = _source.values.toList()[0] ? 'Mobile: Online' : 'Mobile: Offline';
debugPrint(string);
_verifyLocalCache();
_configure();
break;
case ConnectivityResult.wifi:
string = _source.values.toList()[0] ? 'WiFi: Online' : 'WiFi: Offline';
debugPrint(string);
_verifyLocalCache();
_configure();
break;
case ConnectivityResult.none:
default:
string = 'Offline';
debugPrint(string);
}
});
}

_verifyLocalCache() async {
final prefs = await SharedPreferences.getInstance();
final productId = prefs.getString('productId');
if (productId !=null) {
validateReceipt(productId);
}else{
debugPrint('Local cache empty!');
}
}

_configure() async {
/// For both iOS and Android
authentication('your-site', 'publishable_api_key', 'iOS ResourceID/SDK Key',
'Android ResourceID/SDK Key',);
}
@override
Widget build(BuildContext context) {
mProgressBarUtil = ProgressBarUtil(context);
Expand Down Expand Up @@ -361,6 +402,24 @@ class _MyHomePageState extends State<MyHomePage> {
}
}

Future<void> validateReceipt(String productId) async {
try {
final customer = CBCustomer('', '', '', '',);
final result = await Chargebee.validateReceipt(productId,customer);
debugPrint('subscription result : $result');
debugPrint('subscription id : ${result.subscriptionId}');
debugPrint('plan id : ${result.planId}');
debugPrint('subscription status : ${result.status}');
mProgressBarUtil.hideProgressDialog();
/// if validateReceipt success, clear the cache
final prefs = await SharedPreferences.getInstance();
prefs.remove('productId');
} on PlatformException catch (e) {
debugPrint('Error Message: ${e.message}, Error Details: ${e.details}, Error Code: ${e.code}');
mProgressBarUtil.hideProgressDialog();
}
}

_showDialog(BuildContext context, String message) {
final alert = BaseAlertDialog('Chargebee', message);
showDialog(
Expand Down
28 changes: 28 additions & 0 deletions example/lib/network_connectivity.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';

class NetworkConnectivity {
NetworkConnectivity._();
static final _instance = NetworkConnectivity._();
static NetworkConnectivity get instance => _instance;
final _networkConnectivity = Connectivity();
final _controller = StreamController.broadcast();
Stream get myStream => _controller.stream;

Future<void> initialise() async {
_networkConnectivity.onConnectivityChanged.listen(_checkStatus);
}

Future<void> _checkStatus(ConnectivityResult result) async {
var isOnline = false;
try {
final result = await InternetAddress.lookup('example.com');
isOnline = result.isNotEmpty && result[0].rawAddress.isNotEmpty;
} on SocketException catch (_) {
isOnline = false;
}
_controller.sink.add({result: isOnline});
}
void disposeStream() => _controller.close();
}
30 changes: 30 additions & 0 deletions example/lib/product_listview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:chargebee_flutter/chargebee_flutter.dart';
import 'package:chargebee_flutter_sdk_example/progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

class ProductListView extends StatefulWidget {
final List<Product> listProducts;
Expand Down Expand Up @@ -96,6 +97,35 @@ class ProductListViewState extends State<ProductListView> {
} else {
_showSuccessDialog(context, result.subscriptionId);
}
} on PlatformException catch (e) {
debugPrint('Error Message: ${e.message}, Error Details: ${e.details}, Error Code: ${e.code}');
if (e.code.isNotEmpty) {
final responseCode = int.parse(e.code);
if (responseCode >= 500 && responseCode <=599 ) {
/// Cache the productId in SharedPreferences if failed synching with Chargebee.
final prefs = await SharedPreferences.getInstance();
prefs.setString('productId',product.id);
/// validate the receipt
validateReceipt(product.id);
}
}
}
}

Future<void> validateReceipt(String product) async {
try {
final result = await Chargebee.validateReceipt(product);
debugPrint('subscription result : $result');
mProgressBarUtil.hideProgressDialog();

if (result.status == 'true') {
/// if validateReceipt success, clear the cache
final prefs = await SharedPreferences.getInstance();
prefs.remove('productId');
_showSuccessDialog(context, 'Success');
} else {
_showSuccessDialog(context, result.subscriptionId);
}
} on PlatformException catch (e) {
debugPrint('Error Message: ${e.message}, Error Details: ${e.details}, Error Code: ${e.code}');
mProgressBarUtil.hideProgressDialog();
Expand Down
2 changes: 2 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dependencies:
chargebee_flutter:
path: ../
cupertino_icons: ^1.0.2
connectivity_plus: ^2.3.0
shared_preferences: ^2.0.6

dev_dependencies:
integration_test:
Expand Down
Loading

0 comments on commit 5ce3c06

Please sign in to comment.