From 4d2b67d398d18100015656d376612f7c0c8e74b5 Mon Sep 17 00:00:00 2001 From: Haripriyan Date: Wed, 29 Nov 2023 23:48:44 +0530 Subject: [PATCH 1/6] Handles baseplan and offer tokens for billing library 5 --- android/build.gradle | 2 +- .../flutter/sdk/ChargebeeFlutterSdkPlugin.kt | 98 +++++++++++-------- example/integration_test/chargebee_test.dart | 2 +- example/lib/product_listview.dart | 50 ++-------- lib/src/chargebee.dart | 50 +--------- lib/src/constants.dart | 1 + lib/src/models/product.dart | 16 ++- test/chargebee_test.dart | 4 +- 8 files changed, 86 insertions(+), 137 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index a38965f..a6ef65c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -47,7 +47,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.chargebee:chargebee-android:1.2.0' + implementation 'com.chargebee:chargebee-android:2.0.0-beta-1' implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.android.billingclient:billing-ktx:5.2.1' } diff --git a/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt b/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt index 676199c..0cf23be 100644 --- a/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt +++ b/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt @@ -25,16 +25,7 @@ import java.util.* class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel - var mItemsList = ArrayList() - var mPlansList = ArrayList() - var mSkuProductList = ArrayList() - var result: MethodChannel.Result? = null - var mContext: Context? = null - var subscriptionStatus = HashMap() - var subscriptionsList = ArrayList() - private lateinit var context: Context private lateinit var activity: Activity - var queryParam = arrayOf() override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "chargebee_flutter") @@ -147,12 +138,11 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar productIdList, object : CBCallback.ListProductsCallback> { override fun onSuccess(productDetails: ArrayList) { - mSkuProductList.clear() + var productsList = ArrayList() for (product in productDetails) { - val jsonMapString = Gson().toJson(product.toMap()) - mSkuProductList.add(jsonMapString) + productsList.addAll(product.toProducts()) } - result.success(mSkuProductList) + result.success(productsList) } override fun onError(error: CBException) { @@ -189,6 +179,7 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar ) val arrayList: ArrayList = ArrayList() arrayList.add(args["product"] as String) + val offerToken = args["offerToken"] as String CBPurchase.retrieveProducts( activity, arrayList, @@ -201,8 +192,10 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar ) return } + val purchaseProductParams = + PurchaseProductParams(productIDs.first(), offerToken) CBPurchase.purchaseProduct( - productIDs.first(), + purchaseProductParams, customer, object : CBCallback.PurchaseCallback { override fun onSuccess( @@ -287,6 +280,7 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar private fun onResultMap( id: String, planId: String, customerId: String, status: String ): String { + var subscriptionStatus = HashMap() subscriptionStatus["subscriptionId"] = id subscriptionStatus["planId"] = planId subscriptionStatus["customerId"] = customerId @@ -312,6 +306,7 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar } private fun retrieveAllItems(queryParams: Map? = mapOf(), result: Result) { + var queryParam = arrayOf() if (queryParams != null) queryParam = arrayOf( queryParams["limit"] ?: "", @@ -333,6 +328,7 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar } private fun retrieveAllPlans(queryParams: Map? = mapOf(), result: Result) { + var queryParam = arrayOf() if (queryParams != null) queryParam = arrayOf( queryParams["limit"] ?: "", @@ -357,9 +353,10 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar queryParams: Map? = mapOf(), result: Result ) { + var queryParam = arrayOf() if (queryParams != null) queryParam = arrayOf(queryParams["limit"] ?: "") - CBPurchase.retrieveProductIdentifers(queryParam) { + CBPurchase.retrieveProductIdentifiers(queryParam) { when (it) { is CBProductIDResult.ProductIds -> { if (it.IDs.isNotEmpty()) { @@ -532,40 +529,59 @@ class ChargebeeFlutterSdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar } } -fun CBProduct.toMap(): Map { +private fun CBProduct.toProducts(): List { + var products = mutableListOf() + subscriptionOffers?.forEach{ + products.add(Gson().toJson(it.toMap(this))) + } + oneTimePurchaseOffer?.let { products.add(Gson().toJson(it.toMap(this))) } + return products +} + +private fun PricingPhase.toMap(product: CBProduct): Map { return mapOf( - "productId" to productId, - "productPrice" to convertPriceAmountInMicros(), - "productPriceString" to productPrice, - "productTitle" to productTitle, - "currencyCode" to skuDetails.priceCurrencyCode, - "subscriptionPeriod" to subscriptionPeriod() + "productId" to product.id, + "productTitle" to product.title, + "productPrice" to amountInMicros, + "productPriceString" to formattedPrice, + "currencyCode" to currencyCode, + "subscriptionPeriod" to defaultSubscriptionPeriod() ) } -fun CBProduct.convertPriceAmountInMicros(): Double { - return skuDetails.priceAmountMicros / 1_000_000.0 +fun defaultSubscriptionPeriod(): Map { + return mapOf( + "periodUnit" to "", + "numberOfUnits" to 0 + ) } -fun CBProduct.subscriptionPeriod(): Map { - 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 SubscriptionOffer.toMap(product: CBProduct): Map { + return mapOf( + "productId" to product.id, + "baseProductId" to basePlanId, + "offerId" to offerId.orEmpty(), + "offerToken" to offerToken, + "productTitle" to product.title, + "productPrice" to pricingPhases.first().amountInMicros, + "productPriceString" to pricingPhases.first().formattedPrice, + "currencyCode" to pricingPhases.first().currencyCode, + "subscriptionPeriod" to subscriptionPeriod() + ) +} + +private fun SubscriptionOffer.subscriptionPeriod(): Any { + val subscriptionPricing = this.pricingPhases.last() + val subscriptionPeriod = subscriptionPricing.billingPeriod + val numberOfUnits = subscriptionPeriod?.substring(1, subscriptionPeriod.length - 1)?.toInt() + return mapOf( + "periodUnit" to subscriptionPricing.periodUnit(), + "numberOfUnits" to numberOfUnits + ) } -fun CBProduct.periodUnit(): String { - return when (skuDetails.subscriptionPeriod.last().toString()) { +fun PricingPhase.periodUnit(): String { + return when (this.billingPeriod?.last().toString()) { "Y" -> "year" "M" -> "month" "W" -> "week" diff --git a/example/integration_test/chargebee_test.dart b/example/integration_test/chargebee_test.dart index 8fb4d17..8e0efa6 100644 --- a/example/integration_test/chargebee_test.dart +++ b/example/integration_test/chargebee_test.dart @@ -165,7 +165,7 @@ class ChargebeeTest { _getProduct(productIdForAndroid); } try { - final result = await Chargebee.purchaseStoreProduct(product, customer: customer); + final result = await Chargebee.purchaseProduct(product, customer: customer); debugPrint('purchase result: $result'); expect(result.status, 'true'); tester.printToConsole('Product subscribed successfully!'); diff --git a/example/lib/product_listview.dart b/example/lib/product_listview.dart index 76506a3..9e7125e 100644 --- a/example/lib/product_listview.dart +++ b/example/lib/product_listview.dart @@ -21,6 +21,7 @@ class ProductListViewState extends State { late List listProducts; late var productPrice = ''; late var productId = ''; + late String? baseProductId = ''; late var currencyCode = ''; late ProgressBarUtil mProgressBarUtil; final TextEditingController productIdTextFieldController = @@ -42,11 +43,12 @@ class ProductListViewState extends State { itemBuilder: (context, pos) { productPrice = listProducts[pos].priceString; productId = listProducts[pos].id; + baseProductId = listProducts[pos].baseProductId; currencyCode = listProducts[pos].currencyCode; return Card( child: ListTile( title: Text( - productId, + productId + " " + (baseProductId ?? ''), style: const TextStyle( color: Colors.black54, fontWeight: FontWeight.bold, @@ -85,9 +87,8 @@ class ProductListViewState extends State { final product = listProducts[position]; if (product.subscriptionPeriod.unit.isNotEmpty && product.subscriptionPeriod.numberOfUnits != 0) { - mProgressBarUtil.showProgressDialog(); - // purchaseProduct(product); - purchaseStoreProduct(product); + mProgressBarUtil.showProgressDialog(); + purchaseProduct(product); } else { _showDialog(context, product); } @@ -98,46 +99,7 @@ class ProductListViewState extends State { Future purchaseProduct(Product product) async { try { - final result = await Chargebee.purchaseProduct(product, 'abc'); - debugPrint('subscription result : $result'); - debugPrint('subscription id : ${result.subscriptionId}'); - debugPrint('plan id : ${result.planId}'); - debugPrint('subscription status : ${result.status}'); - - mProgressBarUtil.hideProgressDialog(); - - if (result.status == 'true') { - _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(); - 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 purchaseStoreProduct(Product product) async { - try { - final customer = CBCustomer( - 'abc_flutter_test', - 'fn', - 'ln', - 'abc@gmail.com', - ); - final result = await Chargebee.purchaseStoreProduct(product, customer: customer); + final result = await Chargebee.purchaseProduct(product, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); debugPrint('subscription result : $result'); debugPrint('subscription id : ${result.subscriptionId}'); debugPrint('plan id : ${result.planId}'); diff --git a/lib/src/chargebee.dart b/lib/src/chargebee.dart index 350ed67..76ee029 100644 --- a/lib/src/chargebee.dart +++ b/lib/src/chargebee.dart @@ -70,30 +70,9 @@ class Chargebee { return products; } - /// Buy the product with/without customer id. - /// - /// [product] product object to be passed. - /// - /// [customerId] it can be optional. - /// if passed, the subscription will be created by using customerId in chargebee. - /// if not passed, the value of customerId is same as SubscriptionId. - /// - /// If purchase success [PurchaseResult] object be returned. - /// Throws an [PlatformException] in case of failure. - @Deprecated( - 'This method will be removed in upcoming release, Use purchaseStoreProduct API instead', - ) - static Future purchaseProduct( - Product product, [ - String? customerId = '', - ]) async { - final map = _convertToMap(product, customerId: customerId); - return _purchaseResult(map); - } - /// Buy the product with/without customer data. /// - /// [product] product object to be passed. + /// [product] Product object to be passed. /// /// [customer] it can be optional. /// if passed, the subscription will be created by using customerId in chargebee. @@ -101,7 +80,7 @@ class Chargebee { /// /// If purchase success [PurchaseResult] object be returned. /// Throws an [PlatformException] in case of failure. - static Future purchaseStoreProduct( + static Future purchaseProduct( Product product, { CBCustomer? customer, }) async { @@ -209,21 +188,6 @@ class Chargebee { return subscriptions; } - /// Retrieves available product identifiers. - /// - /// [queryParams] The map value to be passed as queryParams. - /// Example: {"limit": "10"}. - /// - /// The list of product identifiers be returned if api success. - /// Throws an [PlatformException] in case of failure. - @Deprecated( - 'This method will be removed in upcoming release, Use retrieveProductIdentifiers instead', - ) - static Future> retrieveProductIdentifers([ - Map? queryParams, - ]) async => - retrieveProductIdentifiers(queryParams); - /// Retrieves available product identifiers. /// /// [queryParams] The map value to be passed as queryParams. @@ -415,18 +379,12 @@ class Chargebee { static Map _convertToMap( Product product, { - String? customerId = '', CBCustomer? customer, }) { - String? id = ''; - if (customerId?.isNotEmpty ?? false) { - id = customerId; - } else if (customer?.id?.isNotEmpty ?? false) { - id = customer?.id; - } return { Constants.product: product.id, - Constants.customerId: id, + Constants.offerToken: product.offerToken, + Constants.customerId: customer?.id ?? '', Constants.firstName: customer?.firstName ?? '', Constants.lastName: customer?.lastName ?? '', Constants.email: customer?.email ?? '', diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 5a64682..8f3c709 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -14,6 +14,7 @@ class Constants { static const productType = 'product_type'; static const productId = 'productId'; static const applicationId = 'applicationId'; + static const offerToken = 'offerToken'; /// API name for both iOS and Android static const mAuthentication = 'authentication'; diff --git a/lib/src/models/product.dart b/lib/src/models/product.dart index 7b40b3e..426358f 100644 --- a/lib/src/models/product.dart +++ b/lib/src/models/product.dart @@ -5,6 +5,15 @@ class Product { /// Id of the product late String id; + /// For Android, the basePlanId will be returned. + late String? baseProductId; + + /// For Android, the Offer ID will be returned. + late String? offerId; + + /// For Android, the offerToken will be returned. + late String? offerToken; + /// title of the product late String title; @@ -20,7 +29,7 @@ class Product { /// Subscription period, which consists of unit and number of units late SubscriptionPeriod subscriptionPeriod; - Product(this.id, this.price, this.priceString, this.title, this.currencyCode, + Product(this.id, this.baseProductId, this.offerId, this.offerToken, this.price, this.priceString, this.title, this.currencyCode, this.subscriptionPeriod); /// convert json data into Product model @@ -30,6 +39,9 @@ class Product { json['subscriptionPeriod'] as Map); return Product( json['productId'] as String, + json['baseProductId'] as String?, + json['offerId'] as String?, + json['offerToken'] as String?, json['productPrice'] as num, json['productPriceString'] as String, json['productTitle'] as String, @@ -40,7 +52,7 @@ class Product { @override String toString() => - 'Product(id: $id, price: $price, priceString: $priceString title: $title, currencyCode: $currencyCode, subscriptionPeriod: $subscriptionPeriod)'; + 'Product(id: $id, baseProductId: $baseProductId, offerId: $offerId, offerToken: $offerToken, price: $price, priceString: $priceString title: $title, currencyCode: $currencyCode, subscriptionPeriod: $subscriptionPeriod)'; } class SubscriptionPeriod { diff --git a/test/chargebee_test.dart b/test/chargebee_test.dart index 70250aa..d668bf5 100644 --- a/test/chargebee_test.dart +++ b/test/chargebee_test.dart @@ -247,7 +247,7 @@ void main() { test('subscribed with customer info for Android', () async { channelResponse = purchaseResult; debugDefaultTargetPlatformOverride = TargetPlatform.android; - final result = await Chargebee.purchaseStoreProduct(product, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); + final result = await Chargebee.purchaseProduct(product, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); expect(callStack, [ isMethodCall( Constants.mPurchaseProduct, @@ -266,7 +266,7 @@ void main() { test('subscribed with customer info for iOS', () async { channelResponse = purchaseResult; debugDefaultTargetPlatformOverride = TargetPlatform.android; - final result = await Chargebee.purchaseStoreProduct(product, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); + final result = await Chargebee.purchaseProduct(product, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); expect(callStack, [ isMethodCall( Constants.mPurchaseProduct, From 75d1224dcd907ce6b3f13822ed2556e053376d44 Mon Sep 17 00:00:00 2001 From: Haripriyan Date: Thu, 30 Nov 2023 18:21:58 +0530 Subject: [PATCH 2/6] Fixes unit tests --- .../flutter/sdk/ChargebeeFlutterSdkPlugin.kt | 13 +++- example/integration_test/chargebee_test.dart | 2 +- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Podfile | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 22 ++++-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/src/chargebee.dart | 7 +- test/chargebee_test.dart | 74 +++++++++++-------- 8 files changed, 80 insertions(+), 44 deletions(-) diff --git a/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt b/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt index 0cf23be..648fa9b 100644 --- a/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt +++ b/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt @@ -542,7 +542,7 @@ private fun PricingPhase.toMap(product: CBProduct): Map { return mapOf( "productId" to product.id, "productTitle" to product.title, - "productPrice" to amountInMicros, + "productPrice" to convertPriceAmountInMicros(), "productPriceString" to formattedPrice, "currencyCode" to currencyCode, "subscriptionPeriod" to defaultSubscriptionPeriod() @@ -557,15 +557,16 @@ fun defaultSubscriptionPeriod(): Map { } fun SubscriptionOffer.toMap(product: CBProduct): Map { + val pricingPhase = pricingPhases.first() return mapOf( "productId" to product.id, "baseProductId" to basePlanId, "offerId" to offerId.orEmpty(), "offerToken" to offerToken, "productTitle" to product.title, - "productPrice" to pricingPhases.first().amountInMicros, - "productPriceString" to pricingPhases.first().formattedPrice, - "currencyCode" to pricingPhases.first().currencyCode, + "productPrice" to pricingPhase.convertPriceAmountInMicros(), + "productPriceString" to pricingPhase.formattedPrice, + "currencyCode" to pricingPhase.currencyCode, "subscriptionPeriod" to subscriptionPeriod() ) } @@ -606,3 +607,7 @@ internal fun NonSubscription.toMap(): String { ) return Gson().toJson(resultMap) } + +fun PricingPhase.convertPriceAmountInMicros(): Double { + return amountInMicros / 1_000_000.0 +} \ No newline at end of file diff --git a/example/integration_test/chargebee_test.dart b/example/integration_test/chargebee_test.dart index 8e0efa6..d0b5073 100644 --- a/example/integration_test/chargebee_test.dart +++ b/example/integration_test/chargebee_test.dart @@ -129,7 +129,7 @@ class ChargebeeTest { } try { - final result = await Chargebee.purchaseProduct(product, 'abc'); + final result = await Chargebee.purchaseProduct(product, customer: customer); debugPrint('purchase result: $result'); expect(result.status, 'true'); tester.printToConsole('Product subscribed successfully!'); diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f..9625e10 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 1e8c3c9..88359b2 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4a1ca7b..4a4553f 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -199,10 +199,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -213,6 +215,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -339,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -354,6 +357,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 5QPY897A4W; ENABLE_BITCODE = NO; @@ -365,6 +370,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.chargebee.sdk; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -418,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -467,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -484,6 +490,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 5QPY897A4W; ENABLE_BITCODE = NO; @@ -495,6 +503,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.chargebee.sdk; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -508,6 +517,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 5QPY897A4W; ENABLE_BITCODE = NO; @@ -519,6 +530,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.chargebee.sdk; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a..a6b826d 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ {'unit': 'year', 'numberOfUnits': 1}; - final product = Product( + final androidProduct = Product( + 'merchant.pro.android', + 'base.product', + 'offerId', + 'offerToken', + 1500.00, + '1500.00', + 'title', + 'INR', + SubscriptionPeriod.fromMap(map), + ); + final iOSProduct = Product( 'merchant.pro.android', + null, + null, + null, 1500.00, '1500.00', 'title', @@ -185,8 +199,16 @@ void main() { '', '', ); - final params = { - Constants.product: product.id, + final paramsForAndroid = { + Constants.product: androidProduct.id, + Constants.offerToken: androidProduct.offerToken, + Constants.customerId: customer.id ?? '', + Constants.firstName: customer.firstName ?? '', + Constants.lastName: customer.lastName ?? '', + Constants.email: customer.email ?? '', + }; + final paramsForiOS = { + Constants.product: iOSProduct.id, Constants.customerId: customer.id ?? '', Constants.firstName: customer.firstName ?? '', Constants.lastName: customer.lastName ?? '', @@ -195,11 +217,11 @@ void main() { test('returns subscription result for Android', () async { channelResponse = purchaseResult; debugDefaultTargetPlatformOverride = TargetPlatform.android; - final result = await Chargebee.purchaseProduct(product, 'abc'); + final result = await Chargebee.purchaseProduct(androidProduct, customer: customer); expect(callStack, [ isMethodCall( Constants.mPurchaseProduct, - arguments: params, + arguments: paramsForAndroid, ) ]); expect(result.status, 'active'); @@ -208,24 +230,11 @@ void main() { test('returns subscription result for iOS', () async { channelResponse = purchaseResult; debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - final result = await Chargebee.purchaseProduct(product, 'abc'); + final result = await Chargebee.purchaseProduct(iOSProduct, customer:customer); expect(callStack, [ isMethodCall( Constants.mPurchaseProduct, - arguments: params, - ) - ]); - expect(result.status, 'active'); - }); - - test('subscribed with customer id for Android', () async { - channelResponse = purchaseResult; - debugDefaultTargetPlatformOverride = TargetPlatform.android; - final result = await Chargebee.purchaseProduct(product, 'abc'); - expect(callStack, [ - isMethodCall( - Constants.mPurchaseProduct, - arguments: params, + arguments: paramsForiOS, ) ]); expect(result.status, 'active'); @@ -234,11 +243,11 @@ void main() { test('subscribed with customer id for iOS', () async { channelResponse = purchaseResult; debugDefaultTargetPlatformOverride = TargetPlatform.android; - final result = await Chargebee.purchaseProduct(product, 'abc'); + final result = await Chargebee.purchaseProduct(iOSProduct, customer: customer); expect(callStack, [ isMethodCall( Constants.mPurchaseProduct, - arguments: params, + arguments: paramsForiOS, ) ]); expect(result.status, 'active'); @@ -247,12 +256,13 @@ void main() { test('subscribed with customer info for Android', () async { channelResponse = purchaseResult; debugDefaultTargetPlatformOverride = TargetPlatform.android; - final result = await Chargebee.purchaseProduct(product, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); + final result = await Chargebee.purchaseProduct(androidProduct, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); expect(callStack, [ isMethodCall( Constants.mPurchaseProduct, arguments: { - Constants.product: product.id, + Constants.product: androidProduct.id, + Constants.offerToken: androidProduct.offerToken, Constants.customerId: 'abc_flutter_test', Constants.firstName: 'flutter', Constants.lastName: 'test', @@ -266,12 +276,12 @@ void main() { test('subscribed with customer info for iOS', () async { channelResponse = purchaseResult; debugDefaultTargetPlatformOverride = TargetPlatform.android; - final result = await Chargebee.purchaseProduct(product, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); + final result = await Chargebee.purchaseProduct(iOSProduct, customer: CBCustomer('abc_flutter_test', 'flutter', 'test', 'abc@gmail.com')); expect(callStack, [ isMethodCall( Constants.mPurchaseProduct, arguments: { - Constants.product: product.id, + Constants.product: iOSProduct.id, Constants.customerId: 'abc_flutter_test', Constants.firstName: 'flutter', Constants.lastName: 'test', @@ -288,7 +298,7 @@ void main() { code: 'PlatformError', message: 'An error occured',); }); debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - await expectLater(() => Chargebee.purchaseProduct(product), + await expectLater(() => Chargebee.purchaseProduct(iOSProduct), throwsA(isA()),); channel.setMockMethodCallHandler(null); }); @@ -299,7 +309,7 @@ void main() { code: 'PlatformError', message: 'An error occured',); }); debugDefaultTargetPlatformOverride = TargetPlatform.android; - await expectLater(() => Chargebee.purchaseProduct(product), + await expectLater(() => Chargebee.purchaseProduct(androidProduct), throwsA(isA()),); channel.setMockMethodCallHandler(null); }); @@ -837,6 +847,9 @@ void main() { final map = {'unit': 'year', 'numberOfUnits': 1}; final product = Product( 'merchant.pro.android', + 'base.product', + 'offerId', + 'offerToken', 1500.00, '1500.00', 'title', @@ -1011,11 +1024,14 @@ void main() { group('validateReceiptForNonSubscriptions', () { final product = Product( 'merchant.pro.android', + 'base.product', + 'offerId', + 'offerToken', 1500.00, '1500.00', 'title', 'INR', - SubscriptionPeriod.fromMap({'periodUnit': 'month', 'numberOfUnits': 1}), + SubscriptionPeriod.fromMap({'periodUnit': 'month', 'numberOfUnits': 1}), ); const consumableProductType = ProductType.consumable; const nonConsumableProductType = ProductType.non_consumable; From 64de64cea2308360668230ac4909755672e5bb51 Mon Sep 17 00:00:00 2001 From: Haripriyan Date: Thu, 30 Nov 2023 19:33:18 +0530 Subject: [PATCH 3/6] Updates Readme --- CHANGELOG.md | 3 +++ README.md | 9 +++++++-- android/build.gradle | 2 +- ios/chargebee_flutter.podspec | 2 +- lib/src/models/product.dart | 2 +- pubspec.yaml | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c027626..7b1acb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.0.0-beta.1 +* Handles Android Billing library 5 changes to handle base plan and offer tokens + ## 0.4.0 Chore * Updates Android Billing library version from 4.x to 5.2.1 diff --git a/README.md b/README.md index 362ddcd..58193b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Flutter SDK +> [!NOTE] +> #### Updates for Billing Library 5 +> - SDK Version 1.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 0.4.0: This [version](https://github.com/chargebee/chargebee-flutter/tree/master) 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. + Chargebee's Flutter SDK enables you to build a seamless and efficient customer experience for your subscription business. Post-installation, initialization, and authentication with the Chargebee site, this SDK will support the following process. @@ -27,7 +32,7 @@ To use Chargebee SDK in your Flutter app, follow these steps: ``` dart dependencies: - chargebee_flutter: ^0.4.0 + chargebee_flutter: ^1.0.0-beta.1 ``` 2. Install dependency. @@ -100,7 +105,7 @@ Pass the `Product` and `CBCustomer` objects to the following function when the ``` dart try { final customer = CBCustomer('customerId','firstName','lastName','emailId'); - final result = await Chargebee.purchaseStoreProduct(product, customer: customer); + final result = await Chargebee.purchaseProduct(product, customer: customer); print("subscription id : ${result.subscriptionId}"); print("subscription status : ${result.status}"); } on PlatformException catch (e) { diff --git a/android/build.gradle b/android/build.gradle index a6ef65c..568e9f0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ group 'com.chargebee.flutter.sdk' -version '0.4.0' +version '1.0.0-beta.1' buildscript { ext.kotlin_version = '1.6.0' diff --git a/ios/chargebee_flutter.podspec b/ios/chargebee_flutter.podspec index 916dae0..85f0acb 100644 --- a/ios/chargebee_flutter.podspec +++ b/ios/chargebee_flutter.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'chargebee_flutter' - s.version = '0.4.0' + s.version = '1.0.0-beta.1' s.summary = 'This is the official Software Development Kit (SDK) for Chargebee Flutter.' s.description = <<-DESC A new Flutter plugin. diff --git a/lib/src/models/product.dart b/lib/src/models/product.dart index 426358f..7bb8ae2 100644 --- a/lib/src/models/product.dart +++ b/lib/src/models/product.dart @@ -14,7 +14,7 @@ class Product { /// For Android, the offerToken will be returned. late String? offerToken; - /// title of the product + /// Title of the product late String title; /// Currency code for the price diff --git a/pubspec.yaml b/pubspec.yaml index 0568373..cc2784b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: chargebee_flutter description: This is the official Software Development Kit (SDK) for Chargebee Flutter. -version: 0.4.0 +version: 1.0.0-beta.1 homepage: 'https://chargebee.com' repository: 'https://github.com/chargebee/chargebee-flutter' From 7afc2433950bbe1a02aab3b8b6c009bdfc75fc0f Mon Sep 17 00:00:00 2001 From: Haripriyan Date: Fri, 1 Dec 2023 19:29:42 +0530 Subject: [PATCH 4/6] Includes offer price and period for android --- .../flutter/sdk/ChargebeeFlutterSdkPlugin.kt | 31 ++++++++---- example/lib/product_listview.dart | 8 ++- lib/src/models/product.dart | 50 ++++++++++++++++--- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt b/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt index 648fa9b..3bc397a 100644 --- a/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt +++ b/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt @@ -1,7 +1,6 @@ package com.chargebee.flutter.sdk import android.app.Activity -import android.content.Context import android.util.Log import androidx.annotation.NonNull import com.chargebee.android.Chargebee @@ -557,26 +556,38 @@ fun defaultSubscriptionPeriod(): Map { } fun SubscriptionOffer.toMap(product: CBProduct): Map { - val pricingPhase = pricingPhases.first() - return mapOf( + val pricingPhase = pricingPhases.last() + val subscription = mutableMapOf( "productId" to product.id, + "productType" to product.type.value, "baseProductId" to basePlanId, - "offerId" to offerId.orEmpty(), "offerToken" to offerToken, "productTitle" to product.title, "productPrice" to pricingPhase.convertPriceAmountInMicros(), "productPriceString" to pricingPhase.formattedPrice, "currencyCode" to pricingPhase.currencyCode, - "subscriptionPeriod" to subscriptionPeriod() + "subscriptionPeriod" to pricingPhase.subscriptionPeriod() ) + offerId?.let { + subscription["offer"] = offer() + } + return subscription } -private fun SubscriptionOffer.subscriptionPeriod(): Any { - val subscriptionPricing = this.pricingPhases.last() - val subscriptionPeriod = subscriptionPricing.billingPeriod - val numberOfUnits = subscriptionPeriod?.substring(1, subscriptionPeriod.length - 1)?.toInt() +private fun SubscriptionOffer.offer(): Map { + val pricingPhase = pricingPhases.first() + return mapOf( + "id" to offerId.orEmpty(), + "price" to pricingPhase.convertPriceAmountInMicros(), + "priceString" to pricingPhase.formattedPrice, + "period" to pricingPhase.subscriptionPeriod() + ) +} +private fun PricingPhase.subscriptionPeriod(): Map { + val subscriptionPeriod = billingPeriod + val numberOfUnits = subscriptionPeriod?.substring(1, subscriptionPeriod.length - 1)?.toInt() ?: 0 return mapOf( - "periodUnit" to subscriptionPricing.periodUnit(), + "periodUnit" to periodUnit(), "numberOfUnits" to numberOfUnits ) } diff --git a/example/lib/product_listview.dart b/example/lib/product_listview.dart index 9e7125e..9c837f2 100644 --- a/example/lib/product_listview.dart +++ b/example/lib/product_listview.dart @@ -23,6 +23,7 @@ class ProductListViewState extends State { late var productId = ''; late String? baseProductId = ''; late var currencyCode = ''; + late Offer? offer = null; late ProgressBarUtil mProgressBarUtil; final TextEditingController productIdTextFieldController = TextEditingController(); @@ -45,6 +46,11 @@ class ProductListViewState extends State { productId = listProducts[pos].id; baseProductId = listProducts[pos].baseProductId; currencyCode = listProducts[pos].currencyCode; + offer = listProducts[pos].offer; + var priceToDisplay = productPrice + ' (currencyCode: ' + currencyCode + ')'; + if(offer != null) { + priceToDisplay = offer!.priceString + ' Offer:' + offer!.id ; + } return Card( child: ListTile( title: Text( @@ -56,7 +62,7 @@ class ProductListViewState extends State { ), ), subtitle: Text( - productPrice + ' (currencyCode: ' + currencyCode + ')', + priceToDisplay, style: const TextStyle( fontWeight: FontWeight.normal, fontSize: 15, diff --git a/lib/src/models/product.dart b/lib/src/models/product.dart index 7bb8ae2..3148993 100644 --- a/lib/src/models/product.dart +++ b/lib/src/models/product.dart @@ -5,13 +5,13 @@ class Product { /// Id of the product late String id; - /// For Android, the basePlanId will be returned. + /// For Android, returns the basePlanId late String? baseProductId; - /// For Android, the Offer ID will be returned. - late String? offerId; + /// For Android, returns the Offer details if present + late Offer? offer; - /// For Android, the offerToken will be returned. + /// For Android, returns the offerToken late String? offerToken; /// Title of the product @@ -29,7 +29,7 @@ class Product { /// Subscription period, which consists of unit and number of units late SubscriptionPeriod subscriptionPeriod; - Product(this.id, this.baseProductId, this.offerId, this.offerToken, this.price, this.priceString, this.title, this.currencyCode, + Product(this.id, this.baseProductId, this.offer, this.offerToken, this.price, this.priceString, this.title, this.currencyCode, this.subscriptionPeriod); /// convert json data into Product model @@ -37,10 +37,15 @@ class Product { debugPrint('json: $json'); final subscriptionPeriod = SubscriptionPeriod.fromMap( json['subscriptionPeriod'] as Map); + var offer = null; + if(json['offer'] != null) { + offer = Offer.fromMap( + json['offer'] as Map); + } return Product( json['productId'] as String, json['baseProductId'] as String?, - json['offerId'] as String?, + offer, json['offerToken'] as String?, json['productPrice'] as num, json['productPriceString'] as String, @@ -52,7 +57,7 @@ class Product { @override String toString() => - 'Product(id: $id, baseProductId: $baseProductId, offerId: $offerId, offerToken: $offerToken, price: $price, priceString: $priceString title: $title, currencyCode: $currencyCode, subscriptionPeriod: $subscriptionPeriod)'; + 'Product(id: $id, baseProductId: $baseProductId, offer: $offer, offerToken: $offerToken, price: $price, priceString: $priceString title: $title, currencyCode: $currencyCode, subscriptionPeriod: $subscriptionPeriod)'; } class SubscriptionPeriod { @@ -71,6 +76,37 @@ class SubscriptionPeriod { } } +class Offer { + /// Offer Id + late String id; + + /// Local currency offer price for the product offer in double + late num price; + + /// Local currency offer price for the product offer in string + late String priceString; + + /// Subscription Offer period, which consists of unit and number of units + late SubscriptionPeriod offerPeriod; + + Offer(this.id, this.price, this.priceString, this.offerPeriod); + + /// convert map object into SubscriptionPeriod + factory Offer.fromMap(Map map) { + final offerPeriod = SubscriptionPeriod.fromMap( + map['period'] as Map); + return Offer(map['id'] as String, + map['price'] as num, + map['priceString'] as String, + offerPeriod + ); + } + + @override + String toString() => + 'Offer(id: $id, price: $price, priceString: $priceString, offerPeriod: $offerPeriod)'; +} + /// Store the information related to product subscriptions class PurchaseResult { /// product subscriptions id From c0e7241d5805bf043ac15fcd25081b5a267acb49 Mon Sep 17 00:00:00 2001 From: Haripriyan Date: Fri, 1 Dec 2023 20:59:05 +0530 Subject: [PATCH 5/6] Updates tests --- test/chargebee_test.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/chargebee_test.dart b/test/chargebee_test.dart index f9e7ff5..9554cfe 100644 --- a/test/chargebee_test.dart +++ b/test/chargebee_test.dart @@ -169,10 +169,11 @@ void main() { group('purchaseProduct', () { final map = {'unit': 'year', 'numberOfUnits': 1}; + final offer = Offer('offerId', 10, '10', SubscriptionPeriod.fromMap(map)); final androidProduct = Product( 'merchant.pro.android', 'base.product', - 'offerId', + offer, 'offerToken', 1500.00, '1500.00', @@ -845,10 +846,11 @@ void main() { group('purchaseNonSubscriptionProduct', () { final map = {'unit': 'year', 'numberOfUnits': 1}; + final offer = Offer('offerId', 10, '10', SubscriptionPeriod.fromMap(map)); final product = Product( 'merchant.pro.android', 'base.product', - 'offerId', + offer, 'offerToken', 1500.00, '1500.00', @@ -1022,10 +1024,11 @@ void main() { }); group('validateReceiptForNonSubscriptions', () { + final offer = Offer('offerId', 10, '10', SubscriptionPeriod.fromMap({'periodUnit': 'month', 'numberOfUnits': 1})); final product = Product( 'merchant.pro.android', 'base.product', - 'offerId', + offer, 'offerToken', 1500.00, '1500.00', From a0844e805c6b09e748fb7b2efe2a14bed6d85848 Mon Sep 17 00:00:00 2001 From: Haripriyan Date: Tue, 5 Dec 2023 19:34:02 +0530 Subject: [PATCH 6/6] Updates docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58193b2..0118d75 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > [!NOTE] > #### Updates for Billing Library 5 > - SDK Version 1.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 0.4.0: This [version](https://github.com/chargebee/chargebee-flutter/tree/master) 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 0.4.0: This [version](https://github.com/chargebee/chargebee-flutter/tree/main) 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. Chargebee's Flutter SDK enables you to build a seamless and efficient customer experience for your subscription business.