From 34807fb9e75579436c603305eeb6b1c188823c62 Mon Sep 17 00:00:00 2001 From: Bohdan Krokhmaliuk Date: Thu, 22 Jul 2021 20:16:34 +0300 Subject: [PATCH 1/3] Android part --- CHANGELOG.md | 5 +- lib/flutter_inapp_purchase.dart | 292 ++++++++++++-------------- lib/modules.dart | 19 +- lib/utils.dart | 37 +--- test/flutter_inapp_purchase_test.dart | 194 +++++++++-------- 5 files changed, 258 insertions(+), 289 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f99d188a..921abb0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 5.5.0 ++ DEPRECATED: removed dataAndroid and originalJsonAndroid from PurchasedItem. Use transaction receipt instead as it is the same object. + ## 5.0.2 + Replaced obfuscatedAccountIdAndroid with obfuscatedAccountId in request purchase method [#299](https://github.com/dooboolab/flutter_inapp_purchase/pull/299) @@ -8,7 +11,7 @@ + Support null safety [#275](https://github.com/dooboolab/flutter_inapp_purchase/pull/275) ## 4.0.2 -+ The dart side requires "introductoryPriceCyclesAndroid" to be a int [#268](https://github.com/dooboolab/flutter_inapp_purchase/pull/268) ++ The dart side requires "introductoryPriceCyclesAndroid" to be an int [#268](https://github.com/dooboolab/flutter_inapp_purchase/pull/268) ## 4.0.1 + `platform` dep version `>=2.0.0 <4.0.0` diff --git a/lib/flutter_inapp_purchase.dart b/lib/flutter_inapp_purchase.dart index 80fe0acb..af9effe5 100644 --- a/lib/flutter_inapp_purchase.dart +++ b/lib/flutter_inapp_purchase.dart @@ -50,6 +50,11 @@ class FlutterInappPurchase { return instance; } + PlatformException get _platformException => PlatformException( + code: _platform.operatingSystem, + message: "platform not supported", + ); + @visibleForTesting FlutterInappPurchase.private(Platform platform, {http.Client? client}) : _pf = platform, @@ -59,8 +64,7 @@ class FlutterInappPurchase { /// /// eg, `Android 5.1.1` Future get platformVersion async { - final String? version = await _channel.invokeMethod('getPlatformVersion'); - return version; + return _channel.invokeMethod('getPlatformVersion'); } /// Consumes all items on `Android`. @@ -73,8 +77,8 @@ class FlutterInappPurchase { } else if (_platform.isIOS) { return 'no-ops in ios'; } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// InitConnection prepare iap features for both `Android` and `iOS`. @@ -84,52 +88,47 @@ class FlutterInappPurchase { Future get initConnection async { if (_platform.isAndroid) { await _setPurchaseListener(); - final String? result = await _channel.invokeMethod('initConnection'); - return result; + return _channel.invokeMethod('initConnection'); } else if (_platform.isIOS) { await _setPurchaseListener(); - final String? result = await _channel.invokeMethod('canMakePayments'); - return result; + return _channel.invokeMethod('canMakePayments'); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Retrieves a list of products from the store on `Android` and `iOS`. /// /// `iOS` also returns subscriptions. Future> getProducts(List skus) async { - skus = skus.toList(); + if (skus.isEmpty) return []; if (_platform.isAndroid) { - dynamic result = await _channel.invokeMethod( + final result = await _channel.invokeListMethod( 'getItemsByType', { 'type': EnumUtil.getValueString(_TypeInApp.inapp), 'skus': skus, }, ); - return extractItems(result); + return extractItems(result ?? []); } else if (_platform.isIOS) { - dynamic result = await _channel.invokeMethod( + final result = await _channel.invokeListMethod( 'getItems', - { - 'skus': skus, - }, + {'skus': skus}, ); - - return extractItems(json.encode(result)); + return extractItems(result ?? []); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Retrieves subscriptions on `Android` and `iOS`. /// /// `iOS` also returns non-subscription products. Future> getSubscriptions(List skus) async { - skus = skus.toList(); + if (skus.isEmpty) return []; if (_platform.isAndroid) { - dynamic result = await _channel.invokeMethod( + final result = await _channel.invokeListMethod( 'getItemsByType', { 'type': EnumUtil.getValueString(_TypeInApp.subs), @@ -137,82 +136,73 @@ class FlutterInappPurchase { }, ); - return extractItems(result); + return extractItems(result ?? []); } else if (_platform.isIOS) { - dynamic result = await _channel.invokeMethod( + final result = await _channel.invokeListMethod( 'getItems', - { - 'skus': skus, - }, + {'skus': skus}, ); - return extractItems(json.encode(result)); + return extractItems(result ?? []); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Retrieves the user's purchase history on `Android` and `iOS` regardless of consumption status. /// /// Purchase history includes all types of products. /// Identical to [getAvailablePurchases] on `iOS`. - Future?> getPurchaseHistory() async { + Future> getPurchaseHistory() async { if (_platform.isAndroid) { - Future getInappPurchaseHistory = _channel.invokeMethod( + final getInappPurchaseHistory = _channel.invokeListMethod( 'getPurchaseHistoryByType', - { - 'type': EnumUtil.getValueString(_TypeInApp.inapp), - }, + {'type': EnumUtil.getValueString(_TypeInApp.inapp)}, ); - Future getSubsPurchaseHistory = _channel.invokeMethod( + final getSubsPurchaseHistory = _channel.invokeListMethod( 'getPurchaseHistoryByType', - { - 'type': EnumUtil.getValueString(_TypeInApp.subs), - }, + {'type': EnumUtil.getValueString(_TypeInApp.subs)}, ); - List results = + final results = await Future.wait([getInappPurchaseHistory, getSubsPurchaseHistory]); - return results.reduce((result1, result2) => - extractPurchased(result1)! + extractPurchased(result2)!); + return extractPurchased((results[0] ?? []) + (results[1] ?? [])); } else if (_platform.isIOS) { - dynamic result = await _channel.invokeMethod('getAvailableItems'); + final result = await _channel.invokeListMethod('getAvailableItems'); - return extractPurchased(json.encode(result)); + return extractPurchased(result ?? []); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Get all non-consumed purchases made on `Android` and `iOS`. /// /// This is identical to [getPurchaseHistory] on `iOS` - Future?> getAvailablePurchases() async { + Future> getAvailablePurchases() async { if (_platform.isAndroid) { - dynamic result1 = await _channel.invokeMethod( + final getInAppAvailable = _channel.invokeListMethod( 'getAvailableItemsByType', - { - 'type': EnumUtil.getValueString(_TypeInApp.inapp), - }, + {'type': EnumUtil.getValueString(_TypeInApp.inapp)}, ); - dynamic result2 = await _channel.invokeMethod( + final getSubsAvailable = _channel.invokeListMethod( 'getAvailableItemsByType', - { - 'type': EnumUtil.getValueString(_TypeInApp.subs), - }, + {'type': EnumUtil.getValueString(_TypeInApp.subs)}, ); - return extractPurchased(result1)! + extractPurchased(result2)!; + final results = await Future.wait([getInAppAvailable, getSubsAvailable]); + + return extractPurchased((results[0] ?? []) + (results[1] ?? [])); } else if (_platform.isIOS) { - dynamic result = await _channel.invokeMethod('getAvailableItems'); + final result = await _channel.invokeListMethod('getAvailableItems'); - return extractPurchased(json.encode(result)); + return extractPurchased(result ?? []); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Request a purchase on `Android` or `iOS`. @@ -227,7 +217,7 @@ class FlutterInappPurchase { String? obfuscatedProfileIdAndroid, }) async { if (_platform.isAndroid) { - return await _channel.invokeMethod('buyItemByType', { + return _channel.invokeMethod('buyItemByType', { 'type': EnumUtil.getValueString(_TypeInApp.inapp), 'sku': sku, 'oldSku': null, @@ -237,13 +227,13 @@ class FlutterInappPurchase { 'purchaseToken': purchaseTokenAndroid, }); } else if (_platform.isIOS) { - return await _channel.invokeMethod('buyProduct', { + return _channel.invokeMethod('buyProduct', { 'sku': sku, 'forUser': obfuscatedAccountId, }); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Request a subscription on `Android` or `iOS`. @@ -262,7 +252,7 @@ class FlutterInappPurchase { String? purchaseTokenAndroid, }) async { if (_platform.isAndroid) { - return await _channel.invokeMethod('buyItemByType', { + return _channel.invokeMethod('buyItemByType', { 'type': EnumUtil.getValueString(_TypeInApp.subs), 'sku': sku, 'oldSku': oldSkuAndroid, @@ -272,12 +262,12 @@ class FlutterInappPurchase { 'purchaseToken': purchaseTokenAndroid, }); } else if (_platform.isIOS) { - return await _channel.invokeMethod('buyProduct', { + return _channel.invokeMethod('buyProduct', { 'sku': sku, }); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Add Store Payment (iOS only) @@ -286,8 +276,7 @@ class FlutterInappPurchase { /// @returns {Future} Future getPromotedProductIOS() async { if (_platform.isIOS) { - String? result = await _channel.invokeMethod('getPromotedProduct'); - return result; + return _channel.invokeMethod('getPromotedProduct'); } return null; } @@ -300,8 +289,8 @@ class FlutterInappPurchase { if (_platform.isIOS) { return await _channel.invokeMethod('requestPromotedProduct'); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Buy product with offer @@ -313,15 +302,17 @@ class FlutterInappPurchase { Map withOffer, ) async { if (_platform.isIOS) { - return await _channel - .invokeMethod('requestProductWithOfferIOS', { - 'sku': sku, - 'forUser': forUser, - 'withOffer': withOffer, - }); + return _channel.invokeMethod( + 'requestProductWithOfferIOS', + { + 'sku': sku, + 'forUser': forUser, + 'withOffer': withOffer, + }, + ); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Buy product with quantity @@ -332,14 +323,13 @@ class FlutterInappPurchase { int quantity, ) async { if (_platform.isIOS) { - return await _channel - .invokeMethod('requestPurchaseWithQuantity', { - 'sku': sku, - 'quantity': quantity, - }); + return _channel.invokeMethod( + 'requestPurchaseWithQuantity', + {'sku': sku, 'quantity': quantity}, + ); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Get the pending purchases in IOS. @@ -347,11 +337,9 @@ class FlutterInappPurchase { /// @returns {Future>} Future?> getPendingTransactionsIOS() async { if (_platform.isIOS) { - dynamic result = await _channel.invokeMethod( - 'getPendingTransactions', - ); + final result = await _channel.invokeListMethod('getPendingTransactions'); - return extractPurchased(json.encode(result)); + return extractPurchased(result ?? []); } return []; } @@ -361,17 +349,15 @@ class FlutterInappPurchase { /// No effect on `iOS`, whose iap purchases are consumed at the time of purchase. Future acknowledgePurchaseAndroid(String token) async { if (_platform.isAndroid) { - String? result = - await _channel.invokeMethod('acknowledgePurchase', { - 'token': token, - }); - - return result; + return _channel.invokeMethod( + 'acknowledgePurchase', + {'token': token}, + ); } else if (_platform.isIOS) { return 'no-ops in ios'; } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Consumes a purchase on `Android`. @@ -388,33 +374,27 @@ class FlutterInappPurchase { /// break; Future consumePurchaseAndroid(String token) async { if (_platform.isAndroid) { - String? result = - await _channel.invokeMethod('consumeProduct', { + return _channel.invokeMethod('consumeProduct', { 'token': token, }); - return result; } else if (_platform.isIOS) { return 'no-ops in ios'; } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// End Play Store connection on `Android` and remove iap observer in `iOS`. /// /// Absolutely necessary to call this when done with the payment. Future get endConnection async { - if (_platform.isAndroid) { - final String? result = await _channel.invokeMethod('endConnection'); - _removePurchaseListener(); - return result; - } else if (_platform.isIOS) { - final String? result = await _channel.invokeMethod('endConnection'); + if (_platform.isAndroid || _platform.isIOS) { + final result = await _channel.invokeMethod('endConnection'); _removePurchaseListener(); return result; } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Finish a transaction on `iOS`. @@ -426,44 +406,43 @@ class FlutterInappPurchase { if (_platform.isAndroid) { return 'no ops in android'; } else if (_platform.isIOS) { - String? result = - await _channel.invokeMethod('finishTransaction', { - 'transactionIdentifier': transactionId, - }); - return result; + return _channel.invokeMethod( + 'finishTransaction', + {'transactionIdentifier': transactionId}, + ); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Finish a transaction on both `android` and `iOS`. /// /// Call this after finalizing server-side validation of the reciept. - Future finishTransaction(PurchasedItem purchasedItem, + Future finishTransaction(PurchasedItem purchasedItem, {bool isConsumable = false}) async { if (_platform.isAndroid) { if (isConsumable) { - String? result = - await _channel.invokeMethod('consumeProduct', { - 'token': purchasedItem.purchaseToken, - }); - return result; + final result = await _channel.invokeMapMethod( + 'consumeProduct', + {'token': purchasedItem.purchaseToken}, + ); + return PurchaseResult.fromJSON(Map.from(result ?? {})); } else { - String? result = await _channel - .invokeMethod('acknowledgePurchase', { - 'token': purchasedItem.purchaseToken, - }); - return result; + final result = await _channel.invokeMapMethod( + 'acknowledgePurchase', + {'token': purchasedItem.purchaseToken}, + ); + return PurchaseResult.fromJSON(Map.from(result ?? {})); } } else if (_platform.isIOS) { - String? result = - await _channel.invokeMethod('finishTransaction', { - 'transactionIdentifier': purchasedItem.transactionId, - }); - return result; + final result = await _channel.invokeMapMethod( + 'finishTransaction', + {'transactionIdentifier': purchasedItem.transactionId}, + ); + return PurchaseResult.fromJSON(Map.from(result ?? {})); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Clear all remaining transaction on `iOS`. @@ -473,11 +452,10 @@ class FlutterInappPurchase { if (_platform.isAndroid) { return 'no-ops in android.'; } else if (_platform.isIOS) { - String? result = await _channel.invokeMethod('clearTransaction'); - return result; + return _channel.invokeMethod('clearTransaction'); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Retrieves a list of products that have been attempted to purchase through the App Store `iOS` only. @@ -486,13 +464,13 @@ class FlutterInappPurchase { if (_platform.isAndroid) { return []; } else if (_platform.isIOS) { - dynamic result = - await _channel.invokeMethod('getAppStoreInitiatedProducts'); + final result = + await _channel.invokeListMethod('getAppStoreInitiatedProducts'); - return extractItems(json.encode(result)); + return extractItems(result ?? []); } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Check if a subscription is active on `Android` and `iOS`. @@ -506,10 +484,9 @@ class FlutterInappPurchase { Duration grace: const Duration(days: 3), }) async { if (_platform.isIOS) { - var history = - await (getPurchaseHistory() as FutureOr>); + final history = await getPurchaseHistory(); - for (var purchase in history) { + for (final purchase in history) { Duration difference = DateTime.now().difference(purchase.transactionDate!); if (difference.inMinutes <= (duration + grace).inMinutes && @@ -518,17 +495,16 @@ class FlutterInappPurchase { return false; } else if (_platform.isAndroid) { - var purchases = - await (getAvailablePurchases() as FutureOr>); + var purchases = await getAvailablePurchases(); - for (var purchase in purchases) { + for (final purchase in purchases) { if (purchase.productId == sku) return true; } return false; } - throw PlatformException( - code: _platform.operatingSystem, message: "platform not supported"); + + throw _platformException; } /// Validate receipt in ios @@ -613,15 +589,15 @@ class FlutterInappPurchase { _channel.setMethodCallHandler((MethodCall call) { switch (call.method) { case "purchase-updated": - Map result = jsonDecode(call.arguments); + final result = Map.from(call.arguments ?? {}); _purchaseController!.add(new PurchasedItem.fromJSON(result)); break; case "purchase-error": - Map result = jsonDecode(call.arguments); + final result = Map.from(call.arguments ?? {}); _purchaseErrorController!.add(new PurchaseResult.fromJSON(result)); break; case "connection-updated": - Map result = jsonDecode(call.arguments); + final result = Map.from(call.arguments ?? {}); _connectionController!.add(new ConnectionResult.fromJSON(result)); break; case "iap-promoted-product": diff --git a/lib/modules.dart b/lib/modules.dart index 51a490bf..d111bc49 100644 --- a/lib/modules.dart +++ b/lib/modules.dart @@ -39,7 +39,7 @@ class IAPItem { final String? iconUrl; final String? originalJson; - final String originalPrice; + final double originalPrice; /// Create [IAPItem] from a Map that was previously JSON formatted IAPItem.fromJSON(Map json) @@ -72,7 +72,7 @@ class IAPItem { signatureAndroid = json['signatureAndroid'] as String?, iconUrl = json['iconUrl'] as String?, originalJson = json['originalJson'] as String?, - originalPrice = json['originalPrice'].toString(), + originalPrice = json['originalPrice'], discountsIOS = _extractDiscountIOS(json['discounts']); /// wow, i find if i want to save a IAPItem, there is not "toJson" to cast it into String... @@ -215,12 +215,14 @@ class PurchasedItem { final String? orderId; // Android only - final String? dataAndroid; final String? signatureAndroid; final bool? autoRenewingAndroid; final bool? isAcknowledgedAndroid; final PurchaseState? purchaseStateAndroid; - final String? originalJsonAndroid; + @deprecated + String? get originalJsonAndroid => transactionReceipt; + @deprecated + String? get dataAndroid => transactionReceipt; // iOS only final DateTime? originalTransactionDateIOS; @@ -235,13 +237,11 @@ class PurchasedItem { transactionReceipt = json['transactionReceipt'] as String?, purchaseToken = json['purchaseToken'] as String?, orderId = json['orderId'] as String?, - dataAndroid = json['dataAndroid'] as String?, signatureAndroid = json['signatureAndroid'] as String?, isAcknowledgedAndroid = json['isAcknowledgedAndroid'] as bool?, autoRenewingAndroid = json['autoRenewingAndroid'] as bool?, purchaseStateAndroid = _decodePurchaseStateAndroid(json['purchaseStateAndroid'] as int?), - originalJsonAndroid = json['originalJsonAndroid'] as String?, originalTransactionDateIOS = _extractDate(json['originalTransactionDateIOS']), originalTransactionIdentifierIOS = @@ -260,12 +260,10 @@ class PurchasedItem { 'orderId: $orderId, ' /// android specific - 'dataAndroid: $dataAndroid, ' 'signatureAndroid: $signatureAndroid, ' 'isAcknowledgedAndroid: $isAcknowledgedAndroid, ' 'autoRenewingAndroid: $autoRenewingAndroid, ' 'purchaseStateAndroid: $purchaseStateAndroid, ' - 'originalJsonAndroid: $originalJsonAndroid, ' /// ios specific 'originalTransactionDateIOS: ${originalTransactionDateIOS?.toIso8601String()}, ' @@ -275,10 +273,9 @@ class PurchasedItem { /// Coerce miliseconds since epoch in double, int, or String into DateTime format static DateTime? _extractDate(dynamic timestamp) { - if (timestamp == null) return null; + if (timestamp == null || timestamp is! int) return null; - int _toInt() => double.parse(timestamp.toString()).toInt(); - return DateTime.fromMillisecondsSinceEpoch(_toInt()); + return DateTime.fromMillisecondsSinceEpoch(timestamp); } } diff --git a/lib/utils.dart b/lib/utils.dart index 96cff361..10223d8e 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,48 +1,29 @@ import 'modules.dart'; -import 'dart:convert'; -List extractItems(dynamic result) { - List list = json.decode(result.toString()); - List products = list - .map( - (dynamic product) => IAPItem.fromJSON(product as Map), - ) +List extractItems(List result) { + List products = result + .map((map) => IAPItem.fromJSON(Map.from(map))) .toList(); return products; } -List? extractPurchased(dynamic result) { - List? decoded = json - .decode(result.toString()) +List extractPurchased(List result) { + final purhcased = result .map( - (dynamic product) => - PurchasedItem.fromJSON(product as Map), - ) - .toList(); - - return decoded; -} - - -List? extractResult(dynamic result) { - List? decoded = json - .decode(result.toString()) - .map( - (dynamic product) => - PurchaseResult.fromJSON(product as Map), + (product) => PurchasedItem.fromJSON(Map.from(product)), ) .toList(); - return decoded; + return purhcased; } - class EnumUtil { /// return enum value /// /// example: enum Type {Hoge}, /// String value = EnumUtil.getValueString(Type.Hoge); /// assert(value == "Hoge"); - static String getValueString(dynamic enumType) => enumType.toString().split('.')[1]; + static String getValueString(dynamic enumType) => + enumType.toString().split('.')[1]; } diff --git a/test/flutter_inapp_purchase_test.dart b/test/flutter_inapp_purchase_test.dart index 2a8aa0f6..2c7eda32 100644 --- a/test/flutter_inapp_purchase_test.dart +++ b/test/flutter_inapp_purchase_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; @@ -161,13 +160,14 @@ void main() { final List log = []; List skus = []..add("testsku"); - final dynamic result = """[ + final result = [ { "productId": "com.cooni.point1000", "price": "120", "currency": "JPY", "localizedPrice": "¥120", "title": "1,000", + "originalPrice": 1.235862359273509, "description": "1000 points 1000P", "introductoryPrice": "1001", "introductoryPricePaymentModeIOS": "1002", @@ -180,7 +180,7 @@ void main() { "introductoryPricePeriodAndroid": "5", "freeTrialPeriodAndroid": "6" } - ]"""; + ]; setUp(() { FlutterInappPurchase(FlutterInappPurchase.private( @@ -213,9 +213,11 @@ void main() { test('returns correct result', () async { List products = await FlutterInappPurchase.instance.getProducts(skus); - List expected = (json.decode(result) as List) + List expected = (result) .map( - (product) => IAPItem.fromJSON(product as Map), + (product) => IAPItem.fromJSON( + Map.from(product), + ), ) .toList(); for (var i = 0; i < products.length; ++i) { @@ -260,6 +262,7 @@ void main() { "currency": "JPY", "localizedPrice": "¥120", "title": "1,000", + "originalPrice": 1.235862359273509, "description": "1000 points 1000P", "introductoryPrice": "1001", "introductoryPricePaymentModeIOS": "1002", @@ -306,7 +309,9 @@ void main() { await FlutterInappPurchase.instance.getProducts(skus); List? expected = result .map( - (product) => IAPItem.fromJSON(product as Map), + (product) => IAPItem.fromJSON( + Map.from(product), + ), ) .toList(); for (var i = 0; i < products.length; ++i) { @@ -346,13 +351,14 @@ void main() { final List log = []; List skus = []..add("testsku"); - final dynamic result = """[ + final result = [ { "productId": "com.cooni.point1000", "price": "120", "currency": "JPY", "localizedPrice": "¥120", "title": "1,000", + "originalPrice": 1.235862359273509, "description": "1000 points 1000P", "introductoryPrice": "1001", "introductoryPricePaymentModeIOS": "1002", @@ -365,7 +371,7 @@ void main() { "introductoryPricePeriodAndroid": "5", "freeTrialPeriodAndroid": "6" } - ]"""; + ]; setUp(() { FlutterInappPurchase(FlutterInappPurchase.private( @@ -397,9 +403,10 @@ void main() { test('returns correct result', () async { List products = await FlutterInappPurchase.instance.getSubscriptions(skus); - List expected = (json.decode(result) as List) + List expected = result .map( - (product) => IAPItem.fromJSON(product as Map), + (product) => + IAPItem.fromJSON(Map.from(product)), ) .toList(); for (var i = 0; i < products.length; ++i) { @@ -444,6 +451,7 @@ void main() { "currency": "JPY", "localizedPrice": "¥120", "title": "1,000", + "originalPrice": 1.235862359273509, "description": "1000 points 1000P", "introductoryPrice": "1001", "introductoryPricePaymentModeIOS": "1002", @@ -490,7 +498,9 @@ void main() { await FlutterInappPurchase.instance.getSubscriptions(skus); List? expected = result .map( - (product) => IAPItem.fromJSON(product as Map), + (product) => IAPItem.fromJSON( + Map.from(product), + ), ) .toList(); for (var i = 0; i < products.length; ++i) { @@ -529,30 +539,36 @@ void main() { group('for Android', () { final List log = []; - final String resultInapp = """[{ - "transactionDate":"1552824902000", - "transactionId":"testTransactionId", - "productId":"com.cooni.point1000", - "transactionReceipt":"testTransactionReciept", - "purchaseToken":"testPurchaseToken", - "autoRenewingAndroid":true, - "dataAndroid":"testDataAndroid", - "signatureAndroid":"testSignatureAndroid", - "originalTransactionDateIOS":"1552831136000", - "originalTransactionIdentifierIOS":"testOriginalTransactionIdentifierIOS" - }]"""; - final String resultSubs = """[{ - "transactionDate":"1552824902000", - "transactionId":"testSubsTransactionId", - "productId":"com.cooni.point1000.subs", - "transactionReceipt":"testSubsTransactionReciept", - "purchaseToken":"testSubsPurchaseToken", - "autoRenewingAndroid":true, - "dataAndroid":"testSubsDataAndroid", - "signatureAndroid":"testSubsSignatureAndroid", - "originalTransactionDateIOS":"1552831136000", - "originalTransactionIdentifierIOS":"testSubsOriginalTransactionIdentifierIOS" - }]"""; + final resultInApp = [ + { + "transactionDate": 1552824902000, + "transactionId": "testTransactionId", + "productId": "com.cooni.point1000", + "transactionReceipt": "testTransactionReciept", + "purchaseToken": "testPurchaseToken", + "autoRenewingAndroid": true, + "dataAndroid": "testDataAndroid", + "signatureAndroid": "testSignatureAndroid", + "originalTransactionDateIOS": 1552831136000, + "originalTransactionIdentifierIOS": + "testOriginalTransactionIdentifierIOS" + } + ]; + final resultSubs = [ + { + "transactionDate": 1552824902000, + "transactionId": "testSubsTransactionId", + "productId": "com.cooni.point1000.subs", + "transactionReceipt": "testSubsTransactionReciept", + "purchaseToken": "testSubsPurchaseToken", + "autoRenewingAndroid": true, + "dataAndroid": "testSubsDataAndroid", + "signatureAndroid": "testSubsSignatureAndroid", + "originalTransactionDateIOS": 1552831136000, + "originalTransactionIdentifierIOS": + "testSubsOriginalTransactionIdentifierIOS" + } + ]; setUp(() { FlutterInappPurchase(FlutterInappPurchase.private( @@ -563,7 +579,7 @@ void main() { log.add(methodCall); var m = methodCall.arguments as Map; if (m['type'] == 'inapp') { - return resultInapp; + return resultInApp; } else if (m['type'] == 'subs') { return resultSubs; } @@ -580,26 +596,21 @@ void main() { expect(log, [ isMethodCall( 'getPurchaseHistoryByType', - arguments: { - 'type': 'inapp', - }, + arguments: {'type': 'inapp'}, ), isMethodCall( 'getPurchaseHistoryByType', - arguments: { - 'type': 'subs', - }, + arguments: {'type': 'subs'}, ), ]); }); test('returns correct result', () async { - List? actualList = await (FlutterInappPurchase.instance - .getPurchaseHistory() as FutureOr?>) ?? - []; - List expectList = ((json.decode(resultInapp) as List) + - (json.decode(resultSubs) as List)) - .map((item) => PurchasedItem.fromJSON(item)) + List? actualList = + await FlutterInappPurchase.instance.getPurchaseHistory(); + List expectList = (resultInApp + resultSubs) + .map((item) => + PurchasedItem.fromJSON(Map.from(item))) .toList(); for (var i = 0; i < actualList.length; ++i) { @@ -612,7 +623,6 @@ void main() { expect(actual.transactionReceipt, expected.transactionReceipt); expect(actual.purchaseToken, expected.purchaseToken); expect(actual.autoRenewingAndroid, expected.autoRenewingAndroid); - expect(actual.dataAndroid, expected.dataAndroid); expect(actual.signatureAndroid, expected.signatureAndroid); expect(actual.originalTransactionDateIOS, expected.originalTransactionDateIOS); @@ -680,9 +690,8 @@ void main() { }); test('returns correct result', () async { - List? actualList = await (FlutterInappPurchase.instance - .getPurchaseHistory() as FutureOr?>) ?? - []; + List? actualList = + await FlutterInappPurchase.instance.getPurchaseHistory(); List? expectList = result .map((item) => PurchasedItem.fromJSON(item)) .toList(); @@ -697,7 +706,6 @@ void main() { expect(actual.transactionReceipt, expected.transactionReceipt); expect(actual.purchaseToken, expected.purchaseToken); expect(actual.autoRenewingAndroid, expected.autoRenewingAndroid); - expect(actual.dataAndroid, expected.dataAndroid); expect(actual.signatureAndroid, expected.signatureAndroid); expect(actual.originalTransactionDateIOS, expected.originalTransactionDateIOS); @@ -711,31 +719,36 @@ void main() { group('getAvailablePurchases', () { group('for Android', () { final List log = []; - - final String resultInapp = """[{ - "transactionDate":"1552824902000", - "transactionId":"testTransactionId", - "productId":"com.cooni.point1000", - "transactionReceipt":"testTransactionReciept", - "purchaseToken":"testPurchaseToken", - "autoRenewingAndroid":true, - "dataAndroid":"testDataAndroid", - "signatureAndroid":"testSignatureAndroid", - "originalTransactionDateIOS":"1552831136000", - "originalTransactionIdentifierIOS":"testOriginalTransactionIdentifierIOS" - }]"""; - final String resultSubs = """[{ - "transactionDate":"1552824902000", - "transactionId":"testSubsTransactionId", - "productId":"com.cooni.point1000.subs", - "transactionReceipt":"testSubsTransactionReciept", - "purchaseToken":"testSubsPurchaseToken", - "autoRenewingAndroid":true, - "dataAndroid":"testSubsDataAndroid", - "signatureAndroid":"testSubsSignatureAndroid", - "originalTransactionDateIOS":"1552831136000", - "originalTransactionIdentifierIOS":"testSubsOriginalTransactionIdentifierIOS" - }]"""; + final resultInApp = [ + { + "transactionDate": 1552824902000, + "transactionId": "testTransactionId", + "productId": "com.cooni.point1000", + "transactionReceipt": "testTransactionReciept", + "purchaseToken": "testPurchaseToken", + "autoRenewingAndroid": true, + "dataAndroid": "testDataAndroid", + "signatureAndroid": "testSignatureAndroid", + "originalTransactionDateIOS": 1552831136000, + "originalTransactionIdentifierIOS": + "testOriginalTransactionIdentifierIOS" + } + ]; + final resultSubs = [ + { + "transactionDate": 1552824902000, + "transactionId": "testSubsTransactionId", + "productId": "com.cooni.point1000.subs", + "transactionReceipt": "testSubsTransactionReciept", + "purchaseToken": "testSubsPurchaseToken", + "autoRenewingAndroid": true, + "dataAndroid": "testSubsDataAndroid", + "signatureAndroid": "testSubsSignatureAndroid", + "originalTransactionDateIOS": 1552831136000, + "originalTransactionIdentifierIOS": + "testSubsOriginalTransactionIdentifierIOS" + } + ]; setUp(() { FlutterInappPurchase(FlutterInappPurchase.private( @@ -746,7 +759,7 @@ void main() { log.add(methodCall); var m = methodCall.arguments as Map; if (m['type'] == 'inapp') { - return resultInapp; + return resultInApp; } else if (m['type'] == 'subs') { return resultSubs; } @@ -777,12 +790,11 @@ void main() { }); test('returns correct result', () async { - List? actualList = await (FlutterInappPurchase.instance - .getAvailablePurchases() as FutureOr?>) ?? - []; - List expectList = ((json.decode(resultInapp) as List) + - (json.decode(resultSubs) as List)) - .map((item) => PurchasedItem.fromJSON(item)) + List actualList = + await FlutterInappPurchase.instance.getAvailablePurchases(); + List expectList = (resultInApp + resultSubs) + .map((item) => + PurchasedItem.fromJSON(Map.from(item))) .toList(); for (var i = 0; i < actualList.length; ++i) { @@ -795,7 +807,6 @@ void main() { expect(actual.transactionReceipt, expected.transactionReceipt); expect(actual.purchaseToken, expected.purchaseToken); expect(actual.autoRenewingAndroid, expected.autoRenewingAndroid); - expect(actual.dataAndroid, expected.dataAndroid); expect(actual.signatureAndroid, expected.signatureAndroid); expect(actual.originalTransactionDateIOS, expected.originalTransactionDateIOS); @@ -863,9 +874,8 @@ void main() { }); test('returns correct result', () async { - List? actualList = await (FlutterInappPurchase.instance - .getAvailablePurchases() as FutureOr?>) ?? - []; + List? actualList = + await FlutterInappPurchase.instance.getAvailablePurchases(); List? expectList = result .map((item) => PurchasedItem.fromJSON(item as Map)) @@ -881,7 +891,6 @@ void main() { expect(actual.transactionReceipt, expected.transactionReceipt); expect(actual.purchaseToken, expected.purchaseToken); expect(actual.autoRenewingAndroid, expected.autoRenewingAndroid); - expect(actual.dataAndroid, expected.dataAndroid); expect(actual.signatureAndroid, expected.signatureAndroid); expect(actual.originalTransactionDateIOS, expected.originalTransactionDateIOS); @@ -1315,6 +1324,7 @@ void main() { "title": "1,000", "description": "1000 points 1000P", "introductoryPrice": "1001", + "originalPrice": 1.235862359273509, "introductoryPricePaymentModeIOS": "1002", "introductoryPriceNumberOfPeriodsIOS": "1003", "introductoryPriceSubscriptionPeriodIOS": "1004", @@ -1353,7 +1363,9 @@ void main() { .getAppStoreInitiatedProducts(); List? expected = result .map( - (product) => IAPItem.fromJSON(product as Map), + (product) => IAPItem.fromJSON( + Map.from(product), + ), ) .toList(); for (var i = 0; i < products.length; ++i) { From e88f5a9005d758cd087ca4f48b0c79658880521f Mon Sep 17 00:00:00 2001 From: Bohdan Krokhmaliuk Date: Thu, 22 Jul 2021 20:22:20 +0300 Subject: [PATCH 2/3] up --- CHANGELOG.md | 2 +- .../AndroidInappPurchasePlugin.java | 377 ++++++++---------- 2 files changed, 162 insertions(+), 217 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 921abb0c..7e7d6556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## 5.5.0 -+ DEPRECATED: removed dataAndroid and originalJsonAndroid from PurchasedItem. Use transaction receipt instead as it is the same object. ++ DEPRECATED: removed dataAndroid and originalJsonAndroid from PurchasedItem. Use transactionReceipt instead as it is the same object. ## 5.0.2 + Replaced obfuscatedAccountIdAndroid with obfuscatedAccountId in request purchase method [#299](https://github.com/dooboolab/flutter_inapp_purchase/pull/299) diff --git a/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java b/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java index cbf747ba..57f37f4f 100644 --- a/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java +++ b/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java @@ -1,5 +1,6 @@ package com.dooboolab.flutterinapppurchase; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.app.Activity; @@ -24,14 +25,10 @@ import com.android.billingclient.api.SkuDetailsParams; import com.android.billingclient.api.SkuDetailsResponseListener; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.FlutterException; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -106,7 +103,7 @@ public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override - public void onMethodCall(final MethodCall call, final Result result) { + public void onMethodCall(final MethodCall call, final @NonNull Result result) { if (call.method.equals("getPlatformVersion")) { try { result.success("Android " + android.os.Build.VERSION.RELEASE); @@ -127,44 +124,35 @@ else if (call.method.equals("initConnection")) { billingClient = BillingClient.newBuilder(context).setListener(purchasesUpdatedListener) .enablePendingPurchases() .build(); + billingClient.startConnection(new BillingClientStateListener() { private boolean alreadyFinished = false; @Override - public void onBillingSetupFinished(BillingResult billingResult) { - try { - int responseCode = billingResult.getResponseCode(); - - if (responseCode == BillingClient.BillingResponseCode.OK) { - JSONObject item = new JSONObject(); - item.put("connected", true); - channel.invokeMethod("connection-updated", item.toString()); - if (alreadyFinished) return; - alreadyFinished = true; - result.success("Billing client ready"); - } else { - JSONObject item = new JSONObject(); - item.put("connected", false); - channel.invokeMethod("connection-updated", item.toString()); - if (alreadyFinished) return; - alreadyFinished = true; - result.error(call.method, "responseCode: " + responseCode, ""); - } - } catch (JSONException je) { - je.printStackTrace(); - } + public void onBillingSetupFinished(@NonNull BillingResult billingResult) { + int responseCode = billingResult.getResponseCode(); + HashMap item = new HashMap<>(); + if (responseCode == BillingClient.BillingResponseCode.OK) { + item.put("connected", true); + channel.invokeMethod("connection-updated", item); + if (alreadyFinished) return; + alreadyFinished = true; + result.success("Billing client ready"); + } else { + item.put("connected", false); + channel.invokeMethod("connection-updated", item); + if (alreadyFinished) return; + alreadyFinished = true; + result.error(call.method, "responseCode: " + responseCode, ""); + } } @Override public void onBillingServiceDisconnected() { - try { - JSONObject item = new JSONObject(); + HashMap item = new HashMap<>(); item.put("connected", false); - channel.invokeMethod("connection-updated", item.toString()); - } catch (JSONException je) { - je.printStackTrace(); - } + channel.invokeMethod("connection-updated", item); } }); } @@ -208,11 +196,11 @@ else if (call.method.equals("consumeAllItems")) { final ConsumeResponseListener listener = new ConsumeResponseListener() { @Override - public void onConsumeResponse(BillingResult billingResult, String outToken) { + public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String outToken) { array.add(outToken); if (purchases.size() == array.size()) { try { - result.success(array.toString()); + result.success(array); } catch (FlutterException e) { Log.e(TAG, e.getMessage()); } @@ -236,22 +224,15 @@ else if (call.method.equals("getItemsByType")) { return; } - String type = call.argument("type"); - final ArrayList skuArr = call.argument("skus"); - - - ArrayList skuList = new ArrayList<>(); - - for (int i = 0; i < skuArr.size(); i++) { - skuList.add(skuArr.get(i)); - } + final String type = call.argument("type"); + final ArrayList skuList = call.argument("skus"); SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); params.setSkusList(skuList).setType(type); billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() { @Override - public void onSkuDetailsResponse(BillingResult billingResult, List skuDetailsList) { + public void onSkuDetailsResponse(@NonNull BillingResult billingResult, List skuDetailsList) { int responseCode = billingResult.getResponseCode(); if (responseCode != BillingClient.BillingResponseCode.OK) { String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); @@ -266,30 +247,11 @@ public void onSkuDetailsResponse(BillingResult billingResult, List s } try { - JSONArray items = new JSONArray(); + ArrayList> items = new ArrayList<>(); for (SkuDetails skuDetails : skuDetailsList) { - JSONObject item = new JSONObject(); - item.put("productId", skuDetails.getSku()); - item.put("price", String.valueOf(skuDetails.getPriceAmountMicros() / 1000000f)); - item.put("currency", skuDetails.getPriceCurrencyCode()); - item.put("type", skuDetails.getType()); - item.put("localizedPrice", skuDetails.getPrice()); - item.put("title", skuDetails.getTitle()); - item.put("description", skuDetails.getDescription()); - item.put("introductoryPrice", skuDetails.getIntroductoryPrice()); - item.put("subscriptionPeriodAndroid", skuDetails.getSubscriptionPeriod()); - item.put("freeTrialPeriodAndroid", skuDetails.getFreeTrialPeriod()); - item.put("introductoryPriceCyclesAndroid", skuDetails.getIntroductoryPriceCycles()); - item.put("introductoryPricePeriodAndroid", skuDetails.getIntroductoryPricePeriod()); - // new - item.put("iconUrl", skuDetails.getIconUrl()); - item.put("originalJson", skuDetails.getOriginalJson()); - item.put("originalPrice", skuDetails.getOriginalPriceAmountMicros() / 1000000f); - items.put(item); + items.add(buildSkuDetailsMap(skuDetails)); } - result.success(items.toString()); - } catch (JSONException je) { - je.printStackTrace(); + result.success(items); } catch (FlutterException fe) { result.error(call.method, fe.getMessage(), fe.getLocalizedMessage()); } @@ -308,34 +270,20 @@ else if (call.method.equals("getAvailableItemsByType")) { } final String type = call.argument("type"); - final JSONArray items = new JSONArray(); - Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(type.equals("subs") ? BillingClient.SkuType.SUBS : BillingClient.SkuType.INAPP); + + final Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(type.equals("subs") ? BillingClient.SkuType.SUBS : BillingClient.SkuType.INAPP); final List purchases = purchasesResult.getPurchasesList(); try { if (purchases != null) { + ArrayList> items = new ArrayList<>(); + for (Purchase purchase : purchases) { - JSONObject item = new JSONObject(); - item.put("productId", purchase.getSku()); - item.put("transactionId", purchase.getOrderId()); - item.put("transactionDate", purchase.getPurchaseTime()); - item.put("transactionReceipt", purchase.getOriginalJson()); - item.put("orderId", purchase.getOrderId()); - item.put("purchaseToken", purchase.getPurchaseToken()); - item.put("signatureAndroid", purchase.getSignature()); - item.put("purchaseStateAndroid", purchase.getPurchaseState()); - - if (type.equals(BillingClient.SkuType.INAPP)) { - item.put("isAcknowledgedAndroid", purchase.isAcknowledged()); - } else if (type.equals(BillingClient.SkuType.SUBS)) { - item.put("autoRenewingAndroid", purchase.isAutoRenewing()); - } - items.put(item); + items.add(buildPurchaseMap(purchase)); } - result.success(items.toString()); + + result.success(items); } - } catch (JSONException je) { - result.error(call.method, je.getMessage(), je.getLocalizedMessage()); } catch (FlutterException fe) { result.error(call.method, fe.getMessage(), fe.getLocalizedMessage()); } @@ -350,30 +298,19 @@ else if (call.method.equals("getPurchaseHistoryByType")) { billingClient.queryPurchaseHistoryAsync(type.equals("subs") ? BillingClient.SkuType.SUBS : BillingClient.SkuType.INAPP, new PurchaseHistoryResponseListener() { @Override - public void onPurchaseHistoryResponse(BillingResult billingResult, List purchaseHistoryRecordList) { + public void onPurchaseHistoryResponse(@NonNull BillingResult billingResult, List purchaseHistoryRecordList) { if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); result.error(call.method, errorData[0], errorData[1]); return; } - JSONArray items = new JSONArray(); - - try { - for (PurchaseHistoryRecord purchase : purchaseHistoryRecordList) { - JSONObject item = new JSONObject(); - item.put("productId", purchase.getSku()); - item.put("transactionDate", purchase.getPurchaseTime()); - item.put("transactionReceipt", purchase.getOriginalJson()); - item.put("purchaseToken", purchase.getPurchaseToken()); - item.put("dataAndroid", purchase.getOriginalJson()); - item.put("signatureAndroid", purchase.getSignature()); - items.put(item); - } - result.success(items.toString()); - } catch (JSONException je) { - je.printStackTrace(); + ArrayList> items = new ArrayList<>(); + for (PurchaseHistoryRecord record : purchaseHistoryRecordList) { + items.add(buildPurchaseHistoryRecordMap(record)); } + + result.success(items); } }); } @@ -388,41 +325,17 @@ else if (call.method.equals("buyItemByType")) { return; } + // Not null + final String sku = call.argument("sku"); final String type = call.argument("type"); + final int prorationMode = call.argument("prorationMode"); + + final String obfuscatedAccountId = call.argument("obfuscatedAccountId"); final String obfuscatedProfileId = call.argument("obfuscatedProfileId"); - final String sku = call.argument("sku"); final String oldSku = call.argument("oldSku"); - final int prorationMode = call.argument("prorationMode"); final String purchaseToken = call.argument("purchaseToken"); - BillingFlowParams.Builder builder = BillingFlowParams.newBuilder(); - - if (type.equals(BillingClient.SkuType.SUBS) && oldSku != null && !oldSku.isEmpty()) { - // Subscription upgrade/downgrade - builder.setOldSku(oldSku, purchaseToken); - } - - if (type.equals(BillingClient.SkuType.SUBS) && oldSku != null && !oldSku.isEmpty()) { - // Subscription upgrade/downgrade - if (prorationMode != -1) { - builder.setOldSku(oldSku, purchaseToken); - if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE) { - builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE); - } else if (prorationMode == BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION) { - builder.setReplaceSkusProrationMode(BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION); - } else { - builder.setOldSku(oldSku, purchaseToken); - } - } else { - builder.setOldSku(oldSku, purchaseToken); - } - } - - if (prorationMode != 0 && prorationMode != -1) { - builder.setReplaceSkusProrationMode(prorationMode); - } - SkuDetails selectedSku = null; for (SkuDetails skuDetail : skus) { if (skuDetail.getSku().equals(sku)) { @@ -430,12 +343,22 @@ else if (call.method.equals("buyItemByType")) { break; } } + if (selectedSku == null) { String debugMessage = "The sku was not found. Please fetch products first by calling getItems"; result.error(TAG, "buyItemByType", debugMessage); return; } + BillingFlowParams.Builder builder = BillingFlowParams.newBuilder(); + + // Subscription upgrade/downgrade + if (type.equals(BillingClient.SkuType.SUBS) && oldSku != null && !oldSku.isEmpty() && purchaseToken!=null && !purchaseToken.isEmpty()) { + builder.setOldSku(oldSku, purchaseToken); + } + if (prorationMode > 0) { + builder.setReplaceSkusProrationMode(prorationMode); + } if (obfuscatedAccountId != null) { builder.setObfuscatedAccountId(obfuscatedAccountId); } @@ -445,6 +368,7 @@ else if (call.method.equals("buyItemByType")) { builder.setSkuDetails(selectedSku); BillingFlowParams flowParams = builder.build(); + if (activity != null) { billingClient.launchBillingFlow(activity, flowParams); } @@ -455,35 +379,26 @@ else if (call.method.equals("buyItemByType")) { * arguments: token */ else if (call.method.equals("acknowledgePurchase")) { - final String token = call.argument("token"); - if (billingClient == null || !billingClient.isReady()) { result.error(call.method, "IAP not prepared. Check if Google Play service is available.", ""); return; } + final String token = call.argument("token"); AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(token) .build(); + billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() { @Override - public void onAcknowledgePurchaseResponse(BillingResult billingResult) { - if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { - String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - result.error(call.method, errorData[0], errorData[1]); - return; - } - try { - JSONObject item = new JSONObject(); - item.put("responseCode", billingResult.getResponseCode()); - item.put("debugMessage", billingResult.getDebugMessage()); + public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) { + if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - item.put("code", errorData[0]); - item.put("message", errorData[1]); - result.success(item.toString()); - } catch (JSONException je) { - je.printStackTrace(); + result.error(call.method, errorData[0], errorData[1]); + } else { + final HashMap resultMap = buildBillingResultMap(billingResult); + result.success(resultMap); } } }); @@ -500,89 +415,47 @@ else if (call.method.equals("consumeProduct")) { } final String token = call.argument("token"); - final ConsumeParams params = ConsumeParams.newBuilder() .setPurchaseToken(token) .build(); + billingClient.consumeAsync(params, new ConsumeResponseListener() { @Override - public void onConsumeResponse(BillingResult billingResult, String purchaseToken) { + public void onConsumeResponse(@NonNull BillingResult billingResult,@NonNull String purchaseToken) { if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); result.error(call.method, errorData[0], errorData[1]); - return; - } - - try { - JSONObject item = new JSONObject(); - item.put("responseCode", billingResult.getResponseCode()); - item.put("debugMessage", billingResult.getDebugMessage()); - String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - item.put("code", errorData[0]); - item.put("message", errorData[1]); - result.success(item.toString()); - } catch (JSONException je) { - result.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", je.getMessage()); + } else{ + final HashMap resultMap = buildBillingResultMap(billingResult); + result.success(resultMap); } } }); } - - /* - * else - */ else { result.notImplemented(); } } - private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() { + private final PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() { @Override public void onPurchasesUpdated(BillingResult billingResult, @Nullable List purchases) { - try { - if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { - JSONObject json = new JSONObject(); - json.put("responseCode", billingResult.getResponseCode()); - json.put("debugMessage", billingResult.getDebugMessage()); - String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - json.put("code", errorData[0]); - json.put("message", errorData[1]); - channel.invokeMethod("purchase-error", json.toString()); - return; - } + if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { + final HashMap resultMap = buildBillingResultMap(billingResult); + channel.invokeMethod("purchase-error", resultMap); + return; + } - if (purchases != null) { - for (Purchase purchase : purchases) { - JSONObject item = new JSONObject(); - item.put("productId", purchase.getSku()); - item.put("transactionId", purchase.getOrderId()); - item.put("transactionDate", purchase.getPurchaseTime()); - item.put("transactionReceipt", purchase.getOriginalJson()); - item.put("purchaseToken", purchase.getPurchaseToken()); - item.put("orderId", purchase.getOrderId()); - - item.put("dataAndroid", purchase.getOriginalJson()); - item.put("signatureAndroid", purchase.getSignature()); - item.put("autoRenewingAndroid", purchase.isAutoRenewing()); - item.put("isAcknowledgedAndroid", purchase.isAcknowledged()); - item.put("purchaseStateAndroid", purchase.getPurchaseState()); - item.put("originalJsonAndroid", purchase.getOriginalJson()); - - - channel.invokeMethod("purchase-updated", item.toString()); - } - } else { - JSONObject json = new JSONObject(); - json.put("responseCode", billingResult.getResponseCode()); - json.put("debugMessage", billingResult.getDebugMessage()); - String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - json.put("code", errorData[0]); - json.put("message", "purchases returns null."); - channel.invokeMethod("purchase-error", json.toString()); - } - } catch (JSONException je) { - channel.invokeMethod("purchase-error", je.getMessage()); + if (purchases == null){ + String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); + final HashMap resultMap = buildBillingResultMap(billingResult,errorData[0],"purchases returns null"); + channel.invokeMethod("purchase-error", resultMap); + return; + } + + for (Purchase purchase : purchases) { + channel.invokeMethod("purchase-updated", buildPurchaseMap(purchase)); } } }; @@ -592,7 +465,79 @@ private void endBillingClientConnection() { try { billingClient.endConnection(); billingClient = null; - } catch (Exception ignored) {} + } catch (Exception ignored) { + + } } } -} + + private HashMap buildPurchaseMap(Purchase purchase){ + HashMap map = new HashMap<>(); + + // part of PurchaseHistory object + map.put("productId", purchase.getSku()); + map.put("signatureAndroid", purchase.getSignature()); + map.put("purchaseToken", purchase.getPurchaseToken()); + map.put("transactionDate", purchase.getPurchaseTime()); + map.put("transactionReceipt", purchase.getOriginalJson()); + + // additional fields for purchase + map.put("orderId", purchase.getOrderId()); + map.put("transactionId", purchase.getOrderId()); + map.put("autoRenewingAndroid", purchase.isAutoRenewing()); + map.put("isAcknowledgedAndroid", purchase.isAcknowledged()); + map.put("purchaseStateAndroid", purchase.getPurchaseState()); + + return map; + } + + private HashMap buildPurchaseHistoryRecordMap(PurchaseHistoryRecord record){ + HashMap map = new HashMap<>(); + + map.put("productId", record.getSku()); + map.put("signatureAndroid", record.getSignature()); + map.put("purchaseToken", record.getPurchaseToken()); + map.put("transactionDate", record.getPurchaseTime()); + map.put("transactionReceipt", record.getOriginalJson()); + + return map; + } + + private HashMap buildSkuDetailsMap(SkuDetails skuDetails){ + HashMap map = new HashMap<>(); + + map.put("productId", skuDetails.getSku()); + map.put("price", String.valueOf(skuDetails.getPriceAmountMicros() / 1000000f)); + map.put("currency", skuDetails.getPriceCurrencyCode()); + map.put("type", skuDetails.getType()); + map.put("localizedPrice", skuDetails.getPrice()); + map.put("title", skuDetails.getTitle()); + map.put("description", skuDetails.getDescription()); + map.put("introductoryPrice", skuDetails.getIntroductoryPrice()); + map.put("subscriptionPeriodAndroid", skuDetails.getSubscriptionPeriod()); + map.put("freeTrialPeriodAndroid", skuDetails.getFreeTrialPeriod()); + map.put("introductoryPriceCyclesAndroid", skuDetails.getIntroductoryPriceCycles()); + map.put("introductoryPricePeriodAndroid", skuDetails.getIntroductoryPricePeriod()); + map.put("iconUrl", skuDetails.getIconUrl()); + map.put("originalJson", skuDetails.getOriginalJson()); + map.put("originalPrice", skuDetails.getOriginalPriceAmountMicros() / 1000000f); + + return map; + } + + private HashMap buildBillingResultMap(BillingResult billingResult){ + String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); + return buildBillingResultMap(billingResult, errorData[0], errorData[1]); + } + + private HashMap buildBillingResultMap(BillingResult billingResult, String errorCode, String message){ + HashMap map = new HashMap<>(); + + map.put("responseCode", billingResult.getResponseCode()); + map.put("debugMessage", billingResult.getDebugMessage()); + map.put("message", message); + map.put("code", errorCode); + + return map; + } + } From 981e21436e27bb279f19cb463edfc279aed6a1f4 Mon Sep 17 00:00:00 2001 From: Bohdan Krokhmaliuk Date: Fri, 23 Jul 2021 01:43:36 +0300 Subject: [PATCH 3/3] UP: 1. Updated Amazon file to return acceptable value types by plugin 2. Created FlutterEntitiesBuilder 3. Removed unnecessary try catches --- .../AmazonInappPurchasePlugin.java | 148 ++++++------------ .../AndroidInappPurchasePlugin.java | 119 +++----------- .../FlutterEntitiesBuilder.java | 135 ++++++++++++++++ 3 files changed, 209 insertions(+), 193 deletions(-) create mode 100644 android/src/main/java/com/dooboolab/flutterinapppurchase/FlutterEntitiesBuilder.java diff --git a/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java b/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java index 64cbae7f..c564c0ee 100644 --- a/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java +++ b/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java @@ -4,32 +4,28 @@ import android.content.Context; import android.util.Log; +import androidx.annotation.NonNull; + import com.amazon.device.iap.PurchasingListener; import com.amazon.device.iap.PurchasingService; import com.amazon.device.iap.model.FulfillmentResult; import com.amazon.device.iap.model.Product; import com.amazon.device.iap.model.ProductDataResponse; -import com.amazon.device.iap.model.ProductType; import com.amazon.device.iap.model.PurchaseResponse; import com.amazon.device.iap.model.PurchaseUpdatesResponse; import com.amazon.device.iap.model.Receipt; import com.amazon.device.iap.model.RequestId; import com.amazon.device.iap.model.UserDataResponse; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; -import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -57,14 +53,14 @@ public void setChannel(MethodChannel channel) { } @Override - public void onMethodCall(final MethodCall call, final Result result) { + public void onMethodCall(final @NonNull MethodCall call, final @NonNull Result result) { this.result=result; try { PurchasingService.registerListener(context, purchasesUpdatedListener); - } catch (Exception e) { result.error(call.method, "Call endConnection method if you want to start over.", e.getMessage()); } + if (call.method.equals("getPlatformVersion")) { try { result.success("Android " + android.os.Build.VERSION.RELEASE); @@ -78,7 +74,7 @@ public void onMethodCall(final MethodCall call, final Result result) { result.success("Billing client has ended."); } else if (call.method.equals("consumeAllItems")) { // consumable is a separate type in amazon - result.success("no-ops in amazon"); + result.error("AMAZON_NOT_USED","no-ops in amazon",null); } else if (call.method.equals("getItemsByType")) { Log.d(TAG, "getItemsByType"); String type = call.argument("type"); @@ -91,21 +87,27 @@ public void onMethodCall(final MethodCall call, final Result result) { } PurchasingService.getProductData(productSkus); + final ArrayList> list = new ArrayList<>(); + result.success(list); } else if (call.method.equals("getAvailableItemsByType")) { String type = call.argument("type"); Log.d(TAG, "gaibt="+type); + + final ArrayList> list = new ArrayList<>(); // NOTE: getPurchaseUpdates doesnt return Consumables which are FULFILLED if(type.equals("inapp")) { PurchasingService.getPurchaseUpdates(true); + result.success(list); } else if(type.equals("subs")) { // Subscriptions are retrieved during inapp, so we just return empty list - result.success("[]"); + result.success(list); } else { result.notImplemented(); } } else if (call.method.equals("getPurchaseHistoryByType")) { + final ArrayList> list = new ArrayList<>(); // No equivalent - result.success("[]"); + result.success(list); } else if (call.method.equals("buyItemByType")) { final String type = call.argument("type"); final String obfuscatedAccountId = call.argument("obfuscatedAccountId"); @@ -113,12 +115,15 @@ public void onMethodCall(final MethodCall call, final Result result) { final String sku = call.argument("sku"); final String oldSku = call.argument("oldSku"); final int prorationMode = call.argument("prorationMode"); + Log.d(TAG, "type="+type+"||sku="+sku+"||oldsku="+oldSku); final RequestId requestId = PurchasingService.purchase(sku); Log.d(TAG, "resid="+requestId.toString()); + + result.success(null); } else if (call.method.equals("consumeProduct")) { // consumable is a separate type in amazon - result.success("no-ops in amazon"); + result.error("AMAZON_NOT_USED","no-ops in amazon",null); } else { result.notImplemented(); } @@ -147,49 +152,27 @@ public void onProductDataResponse(ProductDataResponse response) { final Set unavailableSkus = response.getUnavailableSkus(); Log.d(TAG, "onProductDataResponse: " + unavailableSkus.size() + " unavailable skus"); Log.d(TAG, "unavailableSkus="+unavailableSkus.toString()); - JSONArray items = new JSONArray(); - try { - for (Map.Entry skuDetails : productData.entrySet()) { - Product product=skuDetails.getValue(); - NumberFormat format = NumberFormat.getCurrencyInstance(); - - Number number; - try { - number = format.parse(product.getPrice()); - } catch (ParseException e) { - result.error(TAG, "Price Parsing error", e.getMessage()); - return; - } - JSONObject item = new JSONObject(); - item.put("productId", product.getSku()); - item.put("price", number.toString()); - item.put("currency", null); - ProductType productType = product.getProductType(); - switch (productType) { - case ENTITLED: - case CONSUMABLE: - item.put("type", "inapp"); - break; - case SUBSCRIPTION: - item.put("type", "subs"); - break; - } - item.put("localizedPrice", product.getPrice()); - item.put("title", product.getTitle()); - item.put("description", product.getDescription()); - item.put("introductoryPrice", ""); - item.put("subscriptionPeriodAndroid", ""); - item.put("freeTrialPeriodAndroid", ""); - item.put("introductoryPriceCyclesAndroid", 0); - item.put("introductoryPricePeriodAndroid", ""); - Log.d(TAG, "opdr Putting "+item.toString()); - items.put(item); + ArrayList> items = new ArrayList<>(); + + for (Map.Entry skuDetails : productData.entrySet()) { + Product product=skuDetails.getValue(); + NumberFormat format = NumberFormat.getCurrencyInstance(); + + Number number; + try { + number = format.parse(product.getPrice()); + } catch (ParseException e) { + result.error(TAG, "Price Parsing error", e.getMessage()); + return; } - //System.err.println("Sending "+items.toString()); - result.success(items.toString()); - } catch (JSONException e) { - result.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.getMessage()); + + final HashMap item = FlutterEntitiesBuilder.buildSkuDetailsMap(product); + Log.d(TAG, "opdr Putting "+item.toString()); + items.add(item); } + //System.err.println("Sending "+items.toString()); + result.success(items); + break; case FAILED: result.error(TAG,"FAILED",null); @@ -209,19 +192,12 @@ public void onPurchaseResponse(PurchaseResponse response) { case SUCCESSFUL: Receipt receipt = response.getReceipt(); PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED); - Date date = receipt.getPurchaseDate(); - Long transactionDate=date.getTime(); - try { - JSONObject item = getPurchaseData(receipt.getSku(), - receipt.getReceiptId(), - receipt.getReceiptId(), - transactionDate.doubleValue()); - Log.d(TAG, "opr Putting "+item.toString()); - result.success(item.toString()); - channel.invokeMethod("purchase-updated", item.toString()); - } catch (JSONException e) { - result.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.getMessage()); - } + + final HashMap item = FlutterEntitiesBuilder.buildPurchaseMap(receipt); + Log.d(TAG, "opr Putting "+item.toString()); + result.success(item); + channel.invokeMethod("purchase-updated", item); + break; case FAILED: result.error(TAG, "buyItemByType", "billingResponse is not ok: " + status); @@ -237,24 +213,16 @@ public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse response) { switch(status) { case SUCCESSFUL: - JSONArray items = new JSONArray(); - try { - List receipts = response.getReceipts(); - for(Receipt receipt : receipts) { - Date date = receipt.getPurchaseDate(); - Long transactionDate=date.getTime(); - JSONObject item = getPurchaseData(receipt.getSku(), - receipt.getReceiptId(), - receipt.getReceiptId(), - transactionDate.doubleValue()); - - Log.d(TAG, "opudr Putting "+item.toString()); - items.put(item); - } - result.success(items.toString()); - } catch (JSONException e) { - result.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.getMessage()); + ArrayList> items = new ArrayList<>(); + + List receipts = response.getReceipts(); + for(Receipt receipt : receipts) { + final HashMap item = FlutterEntitiesBuilder.buildPurchaseMap(receipt); + Log.d(TAG, "opudr Putting "+item.toString()); + items.add(item); } + result.success(items); + break; case FAILED: result.error(TAG,"FAILED",null); @@ -267,16 +235,4 @@ public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse response) { } }; - JSONObject getPurchaseData(String productId, String transactionId, String transactionReceipt, - Double transactionDate) throws JSONException { - JSONObject item = new JSONObject(); - item.put("productId", productId); - item.put("transactionId", transactionId); - item.put("transactionReceipt", transactionReceipt); - item.put("transactionDate", Double.toString(transactionDate)); - item.put("dataAndroid",null); - item.put("signatureAndroid",null); - item.put("purchaseToken",null); - return item; - } } diff --git a/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java b/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java index 57f37f4f..eb8258fb 100644 --- a/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java +++ b/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java @@ -9,6 +9,7 @@ import android.os.Bundle; import android.util.Log; +import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; @@ -241,20 +242,15 @@ public void onSkuDetailsResponse(@NonNull BillingResult billingResult, List> items = new ArrayList<>(); - for (SkuDetails skuDetails : skuDetailsList) { - items.add(buildSkuDetailsMap(skuDetails)); - } - result.success(items); - } catch (FlutterException fe) { - result.error(call.method, fe.getMessage(), fe.getLocalizedMessage()); + + ArrayList> items = new ArrayList<>(); + for (SkuDetails skuDetails : skuDetailsList) { + items.add(FlutterEntitiesBuilder.buildSkuDetailsMap(skuDetails)); } + result.success(items); } }); } @@ -270,23 +266,18 @@ else if (call.method.equals("getAvailableItemsByType")) { } final String type = call.argument("type"); - final Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(type.equals("subs") ? BillingClient.SkuType.SUBS : BillingClient.SkuType.INAPP); final List purchases = purchasesResult.getPurchasesList(); - try { - if (purchases != null) { - ArrayList> items = new ArrayList<>(); + ArrayList> items = new ArrayList<>(); - for (Purchase purchase : purchases) { - items.add(buildPurchaseMap(purchase)); - } - - result.success(items); + if (purchases != null) { + for (Purchase purchase : purchases) { + items.add(FlutterEntitiesBuilder.buildPurchaseMap(purchase)); } - } catch (FlutterException fe) { - result.error(call.method, fe.getMessage(), fe.getLocalizedMessage()); } + + result.success(items); } /* @@ -307,7 +298,7 @@ public void onPurchaseHistoryResponse(@NonNull BillingResult billingResult, List ArrayList> items = new ArrayList<>(); for (PurchaseHistoryRecord record : purchaseHistoryRecordList) { - items.add(buildPurchaseHistoryRecordMap(record)); + items.add(FlutterEntitiesBuilder.buildPurchaseHistoryRecordMap(record)); } result.success(items); @@ -330,7 +321,6 @@ else if (call.method.equals("buyItemByType")) { final String type = call.argument("type"); final int prorationMode = call.argument("prorationMode"); - final String obfuscatedAccountId = call.argument("obfuscatedAccountId"); final String obfuscatedProfileId = call.argument("obfuscatedProfileId"); final String oldSku = call.argument("oldSku"); @@ -372,6 +362,9 @@ else if (call.method.equals("buyItemByType")) { if (activity != null) { billingClient.launchBillingFlow(activity, flowParams); } + + // Releases async invokeMethod on Flutter side + result.success(null); } /* @@ -397,7 +390,7 @@ public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); result.error(call.method, errorData[0], errorData[1]); } else { - final HashMap resultMap = buildBillingResultMap(billingResult); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult); result.success(resultMap); } } @@ -426,7 +419,7 @@ public void onConsumeResponse(@NonNull BillingResult billingResult,@NonNull Stri String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); result.error(call.method, errorData[0], errorData[1]); } else{ - final HashMap resultMap = buildBillingResultMap(billingResult); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult); result.success(resultMap); } } @@ -442,20 +435,20 @@ public void onConsumeResponse(@NonNull BillingResult billingResult,@NonNull Stri public void onPurchasesUpdated(BillingResult billingResult, @Nullable List purchases) { if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { - final HashMap resultMap = buildBillingResultMap(billingResult); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult); channel.invokeMethod("purchase-error", resultMap); return; } if (purchases == null){ String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - final HashMap resultMap = buildBillingResultMap(billingResult,errorData[0],"purchases returns null"); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult,errorData[0],"purchases returns null"); channel.invokeMethod("purchase-error", resultMap); return; } for (Purchase purchase : purchases) { - channel.invokeMethod("purchase-updated", buildPurchaseMap(purchase)); + channel.invokeMethod("purchase-updated", FlutterEntitiesBuilder.buildPurchaseMap(purchase)); } } }; @@ -471,73 +464,5 @@ private void endBillingClientConnection() { } } - private HashMap buildPurchaseMap(Purchase purchase){ - HashMap map = new HashMap<>(); - // part of PurchaseHistory object - map.put("productId", purchase.getSku()); - map.put("signatureAndroid", purchase.getSignature()); - map.put("purchaseToken", purchase.getPurchaseToken()); - map.put("transactionDate", purchase.getPurchaseTime()); - map.put("transactionReceipt", purchase.getOriginalJson()); - - // additional fields for purchase - map.put("orderId", purchase.getOrderId()); - map.put("transactionId", purchase.getOrderId()); - map.put("autoRenewingAndroid", purchase.isAutoRenewing()); - map.put("isAcknowledgedAndroid", purchase.isAcknowledged()); - map.put("purchaseStateAndroid", purchase.getPurchaseState()); - - return map; - } - - private HashMap buildPurchaseHistoryRecordMap(PurchaseHistoryRecord record){ - HashMap map = new HashMap<>(); - - map.put("productId", record.getSku()); - map.put("signatureAndroid", record.getSignature()); - map.put("purchaseToken", record.getPurchaseToken()); - map.put("transactionDate", record.getPurchaseTime()); - map.put("transactionReceipt", record.getOriginalJson()); - - return map; - } - - private HashMap buildSkuDetailsMap(SkuDetails skuDetails){ - HashMap map = new HashMap<>(); - - map.put("productId", skuDetails.getSku()); - map.put("price", String.valueOf(skuDetails.getPriceAmountMicros() / 1000000f)); - map.put("currency", skuDetails.getPriceCurrencyCode()); - map.put("type", skuDetails.getType()); - map.put("localizedPrice", skuDetails.getPrice()); - map.put("title", skuDetails.getTitle()); - map.put("description", skuDetails.getDescription()); - map.put("introductoryPrice", skuDetails.getIntroductoryPrice()); - map.put("subscriptionPeriodAndroid", skuDetails.getSubscriptionPeriod()); - map.put("freeTrialPeriodAndroid", skuDetails.getFreeTrialPeriod()); - map.put("introductoryPriceCyclesAndroid", skuDetails.getIntroductoryPriceCycles()); - map.put("introductoryPricePeriodAndroid", skuDetails.getIntroductoryPricePeriod()); - map.put("iconUrl", skuDetails.getIconUrl()); - map.put("originalJson", skuDetails.getOriginalJson()); - map.put("originalPrice", skuDetails.getOriginalPriceAmountMicros() / 1000000f); - - return map; - } - - private HashMap buildBillingResultMap(BillingResult billingResult){ - String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - return buildBillingResultMap(billingResult, errorData[0], errorData[1]); - } - - private HashMap buildBillingResultMap(BillingResult billingResult, String errorCode, String message){ - HashMap map = new HashMap<>(); - - map.put("responseCode", billingResult.getResponseCode()); - map.put("debugMessage", billingResult.getDebugMessage()); - map.put("message", message); - map.put("code", errorCode); - - return map; - } } diff --git a/android/src/main/java/com/dooboolab/flutterinapppurchase/FlutterEntitiesBuilder.java b/android/src/main/java/com/dooboolab/flutterinapppurchase/FlutterEntitiesBuilder.java new file mode 100644 index 00000000..4744a0f1 --- /dev/null +++ b/android/src/main/java/com/dooboolab/flutterinapppurchase/FlutterEntitiesBuilder.java @@ -0,0 +1,135 @@ +package com.dooboolab.flutterinapppurchase; + +import com.amazon.device.iap.model.Product; +import com.amazon.device.iap.model.ProductType; +import com.amazon.device.iap.model.Receipt; +import com.android.billingclient.api.AccountIdentifiers; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.PurchaseHistoryRecord; +import com.android.billingclient.api.SkuDetails; + +import java.util.HashMap; + +class FlutterEntitiesBuilder { + static public HashMap buildPurchaseMap(Purchase purchase){ + HashMap map = new HashMap<>(); + + // part of PurchaseHistory object + map.put("productId", purchase.getSku()); + map.put("signatureAndroid", purchase.getSignature()); + map.put("purchaseToken", purchase.getPurchaseToken()); + map.put("transactionDate", purchase.getPurchaseTime()); + map.put("transactionReceipt", purchase.getOriginalJson()); + + // additional fields for purchase + map.put("orderId", purchase.getOrderId()); + map.put("transactionId", purchase.getOrderId()); + map.put("autoRenewingAndroid", purchase.isAutoRenewing()); + map.put("isAcknowledgedAndroid", purchase.isAcknowledged()); + map.put("purchaseStateAndroid", purchase.getPurchaseState()); + + final AccountIdentifiers identifiers = purchase.getAccountIdentifiers(); + if(identifiers!=null){ + map.put("obfuscatedAccountId", identifiers.getObfuscatedAccountId()); + map.put("obfuscatedProfileId", identifiers.getObfuscatedProfileId()); + } + + + return map; + } + + // Amazon + static public HashMap buildPurchaseMap(Receipt receipt){ + HashMap map = new HashMap<>(); + + // part of PurchaseHistory object + map.put("productId", receipt.getSku()); + map.put("transactionDate", receipt.getPurchaseDate().getTime()); + map.put("transactionReceipt", receipt.getReceiptId()); + map.put("transactionId", receipt.getReceiptId()); + + + return map; + } + + static public HashMap buildPurchaseHistoryRecordMap(PurchaseHistoryRecord record){ + HashMap map = new HashMap<>(); + + map.put("productId", record.getSku()); + map.put("signatureAndroid", record.getSignature()); + map.put("purchaseToken", record.getPurchaseToken()); + map.put("transactionDate", record.getPurchaseTime()); + map.put("transactionReceipt", record.getOriginalJson()); + + return map; + } + + static public HashMap buildSkuDetailsMap(SkuDetails skuDetails){ + HashMap map = new HashMap<>(); + + map.put("productId", skuDetails.getSku()); + map.put("price", String.valueOf(skuDetails.getPriceAmountMicros() / 1000000f)); + map.put("currency", skuDetails.getPriceCurrencyCode()); + map.put("type", skuDetails.getType()); + map.put("localizedPrice", skuDetails.getPrice()); + map.put("title", skuDetails.getTitle()); + map.put("description", skuDetails.getDescription()); + map.put("introductoryPrice", skuDetails.getIntroductoryPrice()); + map.put("subscriptionPeriodAndroid", skuDetails.getSubscriptionPeriod()); + map.put("freeTrialPeriodAndroid", skuDetails.getFreeTrialPeriod()); + map.put("introductoryPriceCyclesAndroid", skuDetails.getIntroductoryPriceCycles()); + map.put("introductoryPricePeriodAndroid", skuDetails.getIntroductoryPricePeriod()); + map.put("iconUrl", skuDetails.getIconUrl()); + map.put("originalJson", skuDetails.getOriginalJson()); + map.put("originalPrice", skuDetails.getOriginalPriceAmountMicros() / 1000000f); + + return map; + } + + // Amazon + static public HashMap buildSkuDetailsMap(Product amazonProduct){ + HashMap map = new HashMap<>(); + + map.put("productId", amazonProduct.getSku()); + map.put("price", amazonProduct.toString()); + map.put("currency", null); + map.put("localizedPrice", amazonProduct.getPrice()); + map.put("title", amazonProduct.getTitle()); + map.put("description", amazonProduct.getDescription()); + map.put("introductoryPrice", ""); + map.put("subscriptionPeriodAndroid", ""); + map.put("freeTrialPeriodAndroid", ""); + map.put("introductoryPriceCyclesAndroid", 0); + map.put("introductoryPricePeriodAndroid", ""); + + ProductType productType = amazonProduct.getProductType(); + switch (productType) { + case ENTITLED: + case CONSUMABLE: + map.put("type", "inapp"); + break; + case SUBSCRIPTION: + map.put("type", "subs"); + break; + } + + return map; + } + + static public HashMap buildBillingResultMap(BillingResult billingResult){ + String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); + return buildBillingResultMap(billingResult, errorData[0], errorData[1]); + } + + static public HashMap buildBillingResultMap(BillingResult billingResult, String errorCode, String message){ + HashMap map = new HashMap<>(); + + map.put("responseCode", billingResult.getResponseCode()); + map.put("debugMessage", billingResult.getDebugMessage()); + map.put("message", message); + map.put("code", errorCode); + + return map; + } +}