diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index abafaf506f3a..79f64d5bda53 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.4.0 + +* Migrate to nullsafety. +* Deprecate `sandboxTesting`, introduce `simulatesAskToBuyInSandbox`. +* **Breaking Change:** + * Removed `callbackChannel` in `channels.dart`, see https://github.com/flutter/flutter/issues/69225. + ## 0.3.5+2 * Migrate deprecated references. diff --git a/packages/in_app_purchase/build.yaml b/packages/in_app_purchase/build.yaml index d7b59734f27e..e15cf14b85fd 100644 --- a/packages/in_app_purchase/build.yaml +++ b/packages/in_app_purchase/build.yaml @@ -5,4 +5,3 @@ targets: options: any_map: true create_to_json: true - nullable: false \ No newline at end of file diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 911edae98cfb..82cd509b30be 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -32,7 +32,7 @@ class _MyApp extends StatefulWidget { class _MyAppState extends State<_MyApp> { final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance; - StreamSubscription> _subscription; + late StreamSubscription> _subscription; List _notFoundIds = []; List _products = []; List _purchases = []; @@ -40,11 +40,11 @@ class _MyAppState extends State<_MyApp> { bool _isAvailable = false; bool _purchasePending = false; bool _loading = true; - String _queryProductError; + String? _queryProductError; @override void initState() { - Stream purchaseUpdated = + final Stream> purchaseUpdated = InAppPurchaseConnection.instance.purchaseUpdatedStream; _subscription = purchaseUpdated.listen((purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); @@ -76,7 +76,7 @@ class _MyAppState extends State<_MyApp> { await _connection.queryProductDetails(_kProductIds.toSet()); if (productDetailResponse.error != null) { setState(() { - _queryProductError = productDetailResponse.error.message; + _queryProductError = productDetailResponse.error!.message; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; _purchases = []; @@ -146,7 +146,7 @@ class _MyAppState extends State<_MyApp> { ); } else { stack.add(Center( - child: Text(_queryProductError), + child: Text(_queryProductError!), )); } if (_purchasePending) { @@ -235,7 +235,7 @@ class _MyAppState extends State<_MyApp> { })); productList.addAll(_products.map( (ProductDetails productDetails) { - PurchaseDetails previousPurchase = purchases[productDetails.id]; + PurchaseDetails? previousPurchase = purchases[productDetails.id]; return ListTile( title: Text( productDetails.title, @@ -254,8 +254,7 @@ class _MyAppState extends State<_MyApp> { onPressed: () { PurchaseParam purchaseParam = PurchaseParam( productDetails: productDetails, - applicationUserName: null, - sandboxTesting: true); + applicationUserName: null); if (productDetails.id == _kConsumableId) { _connection.buyConsumable( purchaseParam: purchaseParam, @@ -329,7 +328,7 @@ class _MyAppState extends State<_MyApp> { void deliverProduct(PurchaseDetails purchaseDetails) async { // IMPORTANT!! Always verify a purchase purchase details before delivering the product. if (purchaseDetails.productID == _kConsumableId) { - await ConsumableStore.save(purchaseDetails.purchaseID); + await ConsumableStore.save(purchaseDetails.purchaseID!); List consumables = await ConsumableStore.load(); setState(() { _purchasePending = false; @@ -365,7 +364,7 @@ class _MyAppState extends State<_MyApp> { showPendingUI(); } else { if (purchaseDetails.status == PurchaseStatus.error) { - handleError(purchaseDetails.error); + handleError(purchaseDetails.error!); } else if (purchaseDetails.status == PurchaseStatus.purchased) { bool valid = await _verifyPurchase(purchaseDetails); if (valid) { diff --git a/packages/in_app_purchase/example/pubspec.yaml b/packages/in_app_purchase/example/pubspec.yaml index 9b623a15795a..8c9296dc98c8 100644 --- a/packages/in_app_purchase/example/pubspec.yaml +++ b/packages/in_app_purchase/example/pubspec.yaml @@ -5,11 +5,9 @@ author: Flutter Team dependencies: flutter: sdk: flutter - cupertino_icons: ^0.1.2 - shared_preferences: ^0.5.2 + shared_preferences: ^2.0.0-nullsafety.1 dev_dependencies: - test: ^1.5.2 flutter_driver: sdk: flutter in_app_purchase: @@ -21,11 +19,11 @@ dev_dependencies: path: ../ integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.3.0 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" flutter: ">=1.9.1+hotfix.2" diff --git a/packages/in_app_purchase/example/test_driver/test/integration_test.dart b/packages/in_app_purchase/example/test_driver/test/integration_test.dart index 7a2c21338786..0352d4aaeb2d 100644 --- a/packages/in_app_purchase/example/test_driver/test/integration_test.dart +++ b/packages/in_app_purchase/example/test_driver/test/integration_test.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// @dart = 2.9 import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/in_app_purchase/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/integration_test/in_app_purchase_test.dart index a5bfdb0eb409..aa3430fbc7d2 100644 --- a/packages/in_app_purchase/integration_test/in_app_purchase_test.dart +++ b/packages/in_app_purchase/integration_test/in_app_purchase_test.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// @dart = 2.9 import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m index 92872d91234e..f6bdf0c4f249 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m @@ -2,13 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// -// FIAPReceiptManager.m -// in_app_purchase -// -// Created by Chris Yang on 3/2/19. -// - #import "FIAPReceiptManager.h" #import diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 872a34a94954..9b44ad766a98 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -75,7 +75,7 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar }]; [_paymentQueueHandler startObservingPaymentQueue]; _callbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" binaryMessenger:[registrar messenger]]; return self; } @@ -290,7 +290,7 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { }]; } -#pragma mark - delegates +#pragma mark - delegates: - (void)handleTransactionsUpdated:(NSArray *)transactions { NSMutableArray *maps = [NSMutableArray new]; diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart index 2aa91d9f9225..9f96c05e15f9 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -53,10 +53,7 @@ class BillingClient { bool _enablePendingPurchases = false; /// Creates a billing client. - /// - /// The `onPurchasesUpdated` parameter must not be null. BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { - assert(onPurchasesUpdated != null); channel.setMethodCallHandler(callHandler); _callbacks[kOnPurchasesUpdated] = [onPurchasesUpdated]; } @@ -74,8 +71,11 @@ class BillingClient { /// Calls /// [`BillingClient#isReady()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#isReady()) /// to get the ready status of the BillingClient instance. - Future isReady() async => - await channel.invokeMethod('BillingClient#isReady()'); + Future isReady() async { + final bool? ready = + await channel.invokeMethod('BillingClient#isReady()'); + return ready ?? false; + } /// Enable the [BillingClientWrapper] to handle pending purchases. /// @@ -100,20 +100,21 @@ class BillingClient { /// This triggers the creation of a new `BillingClient` instance in Java if /// one doesn't already exist. Future startConnection( - {@required - OnBillingServiceDisconnected onBillingServiceDisconnected}) async { + {required OnBillingServiceDisconnected + onBillingServiceDisconnected}) async { assert(_enablePendingPurchases, 'enablePendingPurchases() must be called before calling startConnection'); List disconnectCallbacks = _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); - return BillingResultWrapper.fromJson(await channel - .invokeMapMethod( - "BillingClient#startConnection(BillingClientStateListener)", - { - 'handle': disconnectCallbacks.length - 1, - 'enablePendingPurchases': _enablePendingPurchases - })); + return BillingResultWrapper.fromJson((await channel + .invokeMapMethod( + "BillingClient#startConnection(BillingClientStateListener)", + { + 'handle': disconnectCallbacks.length - 1, + 'enablePendingPurchases': _enablePendingPurchases + })) ?? + {}); } /// Calls @@ -137,15 +138,16 @@ class BillingClient { /// `SkuDetailsParams` as direct arguments instead of requiring it constructed /// and passed in as a class. Future querySkuDetails( - {@required SkuType skuType, @required List skusList}) async { + {required SkuType skuType, required List skusList}) async { final Map arguments = { 'skuType': SkuTypeConverter().toJson(skuType), 'skusList': skusList }; - return SkuDetailsResponseWrapper.fromJson(await channel.invokeMapMethod< - String, dynamic>( - 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)', - arguments)); + return SkuDetailsResponseWrapper.fromJson((await channel.invokeMapMethod< + String, dynamic>( + 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)', + arguments)) ?? + {}); } /// Attempt to launch the Play Billing Flow for a given [skuDetails]. @@ -172,16 +174,17 @@ class BillingClient { /// and [the given /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)). Future launchBillingFlow( - {@required String sku, String accountId}) async { + {required String sku, String? accountId}) async { assert(sku != null); final Map arguments = { 'sku': sku, 'accountId': accountId, }; return BillingResultWrapper.fromJson( - await channel.invokeMapMethod( - 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', - arguments)); + (await channel.invokeMapMethod( + 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', + arguments)) ?? + {}); } /// Fetches recent purchases for the given [SkuType]. @@ -197,10 +200,12 @@ class BillingClient { /// skutype)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases). Future queryPurchases(SkuType skuType) async { assert(skuType != null); - return PurchasesResultWrapper.fromJson(await channel - .invokeMapMethod( - 'BillingClient#queryPurchases(String)', - {'skuType': SkuTypeConverter().toJson(skuType)})); + return PurchasesResultWrapper.fromJson((await channel + .invokeMapMethod( + 'BillingClient#queryPurchases(String)', { + 'skuType': SkuTypeConverter().toJson(skuType) + })) ?? + {}); } /// Fetches purchase history for the given [SkuType]. @@ -218,10 +223,13 @@ class BillingClient { /// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync). Future queryPurchaseHistory(SkuType skuType) async { assert(skuType != null); - return PurchasesHistoryResult.fromJson(await channel.invokeMapMethod( - 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', - {'skuType': SkuTypeConverter().toJson(skuType)})); + return PurchasesHistoryResult.fromJson((await channel.invokeMapMethod< + String, dynamic>( + 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', + { + 'skuType': SkuTypeConverter().toJson(skuType) + })) ?? + {}); } /// Consumes a given in-app product. @@ -229,20 +237,20 @@ class BillingClient { /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. /// - /// The `purchaseToken` must not be null. /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. /// /// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) Future consumeAsync(String purchaseToken, - {String developerPayload}) async { + {String? developerPayload}) async { assert(purchaseToken != null); - return BillingResultWrapper.fromJson(await channel - .invokeMapMethod( - 'BillingClient#consumeAsync(String, ConsumeResponseListener)', - { - 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, - })); + return BillingResultWrapper.fromJson((await channel + .invokeMapMethod( + 'BillingClient#consumeAsync(String, ConsumeResponseListener)', + { + 'purchaseToken': purchaseToken, + 'developerPayload': developerPayload, + })) ?? + {}); } /// Acknowledge an in-app purchase. @@ -261,20 +269,20 @@ class BillingClient { /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more /// details. /// - /// The `purchaseToken` must not be null. /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. /// /// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) Future acknowledgePurchase(String purchaseToken, - {String developerPayload}) async { + {String? developerPayload}) async { assert(purchaseToken != null); - return BillingResultWrapper.fromJson(await channel.invokeMapMethod( - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', - { - 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, - })); + return BillingResultWrapper.fromJson((await channel.invokeMapMethod( + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', + { + 'purchaseToken': purchaseToken, + 'developerPayload': developerPayload, + })) ?? + {}); } /// The method call handler for [channel]. @@ -283,15 +291,15 @@ class BillingClient { switch (call.method) { case kOnPurchasesUpdated: // The purchases updated listener is a singleton. - assert(_callbacks[kOnPurchasesUpdated].length == 1); + assert(_callbacks[kOnPurchasesUpdated]!.length == 1); final PurchasesUpdatedListener listener = - _callbacks[kOnPurchasesUpdated].first; + _callbacks[kOnPurchasesUpdated]!.first as PurchasesUpdatedListener; listener(PurchasesResultWrapper.fromJson( call.arguments.cast())); break; case _kOnBillingServiceDisconnected: final int handle = call.arguments['handle']; - await _callbacks[_kOnBillingServiceDisconnected][handle](); + await _callbacks[_kOnBillingServiceDisconnected]![handle](); break; } } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart index 966c89176b1e..30828d8882a7 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart @@ -12,40 +12,50 @@ part 'enum_converters.g.dart'; /// /// Use these in `@JsonSerializable()` classes by annotating them with /// `@BillingResponseConverter()`. -class BillingResponseConverter implements JsonConverter { +class BillingResponseConverter implements JsonConverter { /// Default const constructor. const BillingResponseConverter(); @override - BillingResponse fromJson(int json) => _$enumDecode( - _$BillingResponseEnumMap.cast(), json); + BillingResponse fromJson(int? json) { + if (json == null) { + return BillingResponse.error; + } + return _$enumDecode( + _$BillingResponseEnumMap.cast(), json); + } @override - int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]; + int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; } /// Serializer for [SkuType]. /// /// Use these in `@JsonSerializable()` classes by annotating them with /// `@SkuTypeConverter()`. -class SkuTypeConverter implements JsonConverter { +class SkuTypeConverter implements JsonConverter { /// Default const constructor. const SkuTypeConverter(); @override - SkuType fromJson(String json) => - _$enumDecode(_$SkuTypeEnumMap.cast(), json); + SkuType fromJson(String? json) { + if (json == null) { + return SkuType.inapp; + } + return _$enumDecode( + _$SkuTypeEnumMap.cast(), json); + } @override - String toJson(SkuType object) => _$SkuTypeEnumMap[object]; + String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; } // Define a class so we generate serializer helper methods for the enums @JsonSerializable() class _SerializedEnums { - BillingResponse response; - SkuType type; - PurchaseStateWrapper purchaseState; + late BillingResponse response; + late SkuType type; + late PurchaseStateWrapper purchaseState; } /// Serializer for [PurchaseStateWrapper]. @@ -53,18 +63,23 @@ class _SerializedEnums { /// Use these in `@JsonSerializable()` classes by annotating them with /// `@PurchaseStateConverter()`. class PurchaseStateConverter - implements JsonConverter { + implements JsonConverter { /// Default const constructor. const PurchaseStateConverter(); @override - PurchaseStateWrapper fromJson(int json) => _$enumDecode( - _$PurchaseStateWrapperEnumMap.cast(), - json); + PurchaseStateWrapper fromJson(int? json) { + if (json == null) { + return PurchaseStateWrapper.unspecified_state; + } + return _$enumDecode( + _$PurchaseStateWrapperEnumMap.cast(), + json); + } @override int toJson(PurchaseStateWrapper object) => - _$PurchaseStateWrapperEnumMap[object]; + _$PurchaseStateWrapperEnumMap[object]!; /// Converts the purchase state stored in `object` to a [PurchaseStatus]. /// @@ -78,7 +93,5 @@ class PurchaseStateConverter case PurchaseStateWrapper.unspecified_state: return PurchaseStatus.error; } - - throw ArgumentError('$object isn\'t mapped to PurchaseStatus'); } } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart index 947700df64df..5d59dd8888b7 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart @@ -21,25 +21,30 @@ Map _$_SerializedEnumsToJson(_SerializedEnums instance) => 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, }) { if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); } - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; } const _$BillingResponseEnumMap = { diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart index 8bdd738e7ed3..05472278968a 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -27,26 +27,27 @@ class PurchaseWrapper { /// Creates a purchase wrapper with the given purchase details. @visibleForTesting PurchaseWrapper( - {@required this.orderId, - @required this.packageName, - @required this.purchaseTime, - @required this.purchaseToken, - @required this.signature, - @required this.sku, - @required this.isAutoRenewing, - @required this.originalJson, - @required this.developerPayload, - @required this.isAcknowledged, - @required this.purchaseState}); + {required this.orderId, + required this.packageName, + required this.purchaseTime, + required this.purchaseToken, + required this.signature, + required this.sku, + required this.isAutoRenewing, + required this.originalJson, + this.developerPayload, + required this.isAcknowledged, + required this.purchaseState}); /// Factory for creating a [PurchaseWrapper] from a [Map] with the purchase details. - factory PurchaseWrapper.fromJson(Map map) => _$PurchaseWrapperFromJson(map); + factory PurchaseWrapper.fromJson(Map map) => + _$PurchaseWrapperFromJson(map); @override bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchaseWrapper typedOther = other; + final PurchaseWrapper typedOther = other as PurchaseWrapper; return typedOther.orderId == orderId && typedOther.packageName == packageName && typedOther.purchaseTime == purchaseTime && @@ -74,22 +75,28 @@ class PurchaseWrapper { /// The unique ID for this purchase. Corresponds to the Google Payments order /// ID. + @JsonKey(defaultValue: '') final String orderId; /// The package name the purchase was made from. + @JsonKey(defaultValue: '') final String packageName; /// When the purchase was made, as an epoch timestamp. + @JsonKey(defaultValue: 0) final int purchaseTime; /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + @JsonKey(defaultValue: '') final String purchaseToken; /// Signature of purchase data, signed with the developer's private key. Uses /// RSASSA-PKCS1-v1_5. + @JsonKey(defaultValue: '') final String signature; /// The product ID of this purchase. + @JsonKey(defaultValue: '') final String sku; /// True for subscriptions that renew automatically. Does not apply to @@ -97,6 +104,8 @@ class PurchaseWrapper { /// /// For [SkuType.subs] this means that the subscription is canceled when it is /// false. + /// + /// The value is `false` for [SkuType.inapp] products. final bool isAutoRenewing; /// Details about this purchase, in JSON. @@ -105,15 +114,19 @@ class PurchaseWrapper { /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). /// Note though that verifying a purchase locally is inherently insecure (see /// the article for more details). + @JsonKey(defaultValue: '') final String originalJson; /// The payload specified by the developer when the purchase was acknowledged or consumed. - final String developerPayload; + /// + /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + final String? developerPayload; /// Whether the purchase has been acknowledged. /// /// A successful purchase has to be acknowledged within 3 days after the purchase via [BillingClient.acknowledgePurchase]. /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. + @JsonKey(defaultValue: false) final bool isAcknowledged; /// Determines the current state of the purchase. @@ -137,29 +150,33 @@ class PurchaseHistoryRecordWrapper { /// Creates a [PurchaseHistoryRecordWrapper] with the given record details. @visibleForTesting PurchaseHistoryRecordWrapper({ - @required this.purchaseTime, - @required this.purchaseToken, - @required this.signature, - @required this.sku, - @required this.originalJson, - @required this.developerPayload, + required this.purchaseTime, + required this.purchaseToken, + required this.signature, + required this.sku, + required this.originalJson, + required this.developerPayload, }); /// Factory for creating a [PurchaseHistoryRecordWrapper] from a [Map] with the record details. - factory PurchaseHistoryRecordWrapper.fromJson(Map map) => + factory PurchaseHistoryRecordWrapper.fromJson(Map map) => _$PurchaseHistoryRecordWrapperFromJson(map); /// When the purchase was made, as an epoch timestamp. + @JsonKey(defaultValue: 0) final int purchaseTime; /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + @JsonKey(defaultValue: '') final String purchaseToken; /// Signature of purchase data, signed with the developer's private key. Uses /// RSASSA-PKCS1-v1_5. + @JsonKey(defaultValue: '') final String signature; /// The product ID of this purchase. + @JsonKey(defaultValue: '') final String sku; /// Details about this purchase, in JSON. @@ -168,16 +185,20 @@ class PurchaseHistoryRecordWrapper { /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). /// Note though that verifying a purchase locally is inherently insecure (see /// the article for more details). + @JsonKey(defaultValue: '') final String originalJson; /// The payload specified by the developer when the purchase was acknowledged or consumed. - final String developerPayload; + /// + /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + final String? developerPayload; @override bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchaseHistoryRecordWrapper typedOther = other; + final PurchaseHistoryRecordWrapper typedOther = + other as PurchaseHistoryRecordWrapper; return typedOther.purchaseTime == purchaseTime && typedOther.purchaseToken == purchaseToken && typedOther.signature == signature && @@ -203,9 +224,9 @@ class PurchaseHistoryRecordWrapper { class PurchasesResultWrapper { /// Creates a [PurchasesResultWrapper] with the given purchase result details. PurchasesResultWrapper( - {@required this.responseCode, - @required this.billingResult, - @required this.purchasesList}); + {required this.responseCode, + required this.billingResult, + required this.purchasesList}); /// Factory for creating a [PurchaseResultWrapper] from a [Map] with the result details. factory PurchasesResultWrapper.fromJson(Map map) => @@ -215,7 +236,7 @@ class PurchasesResultWrapper { bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchasesResultWrapper typedOther = other; + final PurchasesResultWrapper typedOther = other as PurchasesResultWrapper; return typedOther.responseCode == responseCode && typedOther.purchasesList == purchasesList && typedOther.billingResult == billingResult; @@ -236,6 +257,7 @@ class PurchasesResultWrapper { /// The list of successful purchases made in this transaction. /// /// May be empty, especially if [responseCode] is not [BillingResponse.ok]. + @JsonKey(defaultValue: []) final List purchasesList; } @@ -248,7 +270,7 @@ class PurchasesResultWrapper { class PurchasesHistoryResult { /// Creates a [PurchasesHistoryResult] with the provided history. PurchasesHistoryResult( - {@required this.billingResult, @required this.purchaseHistoryRecordList}); + {required this.billingResult, required this.purchaseHistoryRecordList}); /// Factory for creating a [PurchasesHistoryResult] from a [Map] with the history result details. factory PurchasesHistoryResult.fromJson(Map map) => @@ -258,7 +280,7 @@ class PurchasesHistoryResult { bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchasesHistoryResult typedOther = other; + final PurchasesHistoryResult typedOther = other as PurchasesHistoryResult; return typedOther.purchaseHistoryRecordList == purchaseHistoryRecordList && typedOther.billingResult == billingResult; } @@ -272,6 +294,7 @@ class PurchasesHistoryResult { /// The list of queried purchase history records. /// /// May be empty, especially if [billingResult.responseCode] is not [BillingResponse.ok]. + @JsonKey(defaultValue: []) final List purchaseHistoryRecordList; } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart index 3d555890b31e..5f0d936e09c2 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -8,18 +8,18 @@ part of 'purchase_wrapper.dart'; PurchaseWrapper _$PurchaseWrapperFromJson(Map json) { return PurchaseWrapper( - orderId: json['orderId'] as String, - packageName: json['packageName'] as String, - purchaseTime: json['purchaseTime'] as int, - purchaseToken: json['purchaseToken'] as String, - signature: json['signature'] as String, - sku: json['sku'] as String, + orderId: json['orderId'] as String? ?? '', + packageName: json['packageName'] as String? ?? '', + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + sku: json['sku'] as String? ?? '', isAutoRenewing: json['isAutoRenewing'] as bool, - originalJson: json['originalJson'] as String, - developerPayload: json['developerPayload'] as String, - isAcknowledged: json['isAcknowledged'] as bool, + originalJson: json['originalJson'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, + isAcknowledged: json['isAcknowledged'] as bool? ?? false, purchaseState: - const PurchaseStateConverter().fromJson(json['purchaseState'] as int), + const PurchaseStateConverter().fromJson(json['purchaseState'] as int?), ); } @@ -41,12 +41,12 @@ Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { return PurchaseHistoryRecordWrapper( - purchaseTime: json['purchaseTime'] as int, - purchaseToken: json['purchaseToken'] as String, - signature: json['signature'] as String, - sku: json['sku'] as String, - originalJson: json['originalJson'] as String, - developerPayload: json['developerPayload'] as String, + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + sku: json['sku'] as String? ?? '', + originalJson: json['originalJson'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, ); } @@ -64,11 +64,16 @@ Map _$PurchaseHistoryRecordWrapperToJson( PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { return PurchasesResultWrapper( responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int), - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - purchasesList: (json['purchasesList'] as List) - .map((e) => PurchaseWrapper.fromJson(e as Map)) - .toList(), + const BillingResponseConverter().fromJson(json['responseCode'] as int?), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchasesList: (json['purchasesList'] as List?) + ?.map((e) => + PurchaseWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], ); } @@ -83,10 +88,16 @@ Map _$PurchasesResultWrapperToJson( PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) { return PurchasesHistoryResult( - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - purchaseHistoryRecordList: (json['purchaseHistoryRecordList'] as List) - .map((e) => PurchaseHistoryRecordWrapper.fromJson(e as Map)) - .toList(), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchaseHistoryRecordList: + (json['purchaseHistoryRecordList'] as List?) + ?.map((e) => PurchaseHistoryRecordWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], ); } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart index db65e2064a14..b3872958e5b9 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -13,6 +13,13 @@ import 'enum_converters.dart'; // rebuild and watch for further changes. part 'sku_details_wrapper.g.dart'; +/// The error message shown when the map represents billing result is invalid from method channel. +/// +/// This usually indicates a series underlining code issue in the plugin. +@visibleForTesting +const kInvalidBillingResultErrorMessage = + 'Invalid billing result map from method channel.'; + /// Dart wrapper around [`com.android.billingclient.api.SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails). /// /// Contains the details of an available product in Google Play Billing. @@ -22,22 +29,22 @@ class SkuDetailsWrapper { /// Creates a [SkuDetailsWrapper] with the given purchase details. @visibleForTesting SkuDetailsWrapper({ - @required this.description, - @required this.freeTrialPeriod, - @required this.introductoryPrice, - @required this.introductoryPriceMicros, - @required this.introductoryPriceCycles, - @required this.introductoryPricePeriod, - @required this.price, - @required this.priceAmountMicros, - @required this.priceCurrencyCode, - @required this.sku, - @required this.subscriptionPeriod, - @required this.title, - @required this.type, - @required this.isRewarded, - @required this.originalPrice, - @required this.originalPriceAmountMicros, + required this.description, + required this.freeTrialPeriod, + required this.introductoryPrice, + required this.introductoryPriceMicros, + required this.introductoryPriceCycles, + required this.introductoryPricePeriod, + required this.price, + required this.priceAmountMicros, + required this.priceCurrencyCode, + required this.sku, + required this.subscriptionPeriod, + required this.title, + required this.type, + required this.isRewarded, + required this.originalPrice, + required this.originalPriceAmountMicros, }); /// Constructs an instance of this from a key value map of data. @@ -45,55 +52,70 @@ class SkuDetailsWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. @visibleForTesting - factory SkuDetailsWrapper.fromJson(Map map) => + factory SkuDetailsWrapper.fromJson(Map map) => _$SkuDetailsWrapperFromJson(map); /// Textual description of the product. + @JsonKey(defaultValue: '') final String description; /// Trial period in ISO 8601 format. + @JsonKey(defaultValue: '') final String freeTrialPeriod; /// Introductory price, only applies to [SkuType.subs]. Formatted ("$0.99"). + @JsonKey(defaultValue: '') final String introductoryPrice; /// [introductoryPrice] in micro-units 990000 + @JsonKey(defaultValue: '') final String introductoryPriceMicros; /// The number of billing perios that [introductoryPrice] is valid for ("2"). + @JsonKey(defaultValue: '') final String introductoryPriceCycles; /// The billing period of [introductoryPrice], in ISO 8601 format. + @JsonKey(defaultValue: '') final String introductoryPricePeriod; /// Formatted with currency symbol ("$0.99"). + @JsonKey(defaultValue: '') final String price; /// [price] in micro-units ("990000"). + @JsonKey(defaultValue: 0) final int priceAmountMicros; /// [price] ISO 4217 currency code. + @JsonKey(defaultValue: '') final String priceCurrencyCode; /// The product ID in Google Play Console. + @JsonKey(defaultValue: '') final String sku; /// Applies to [SkuType.subs], formatted in ISO 8601. + @JsonKey(defaultValue: '') final String subscriptionPeriod; /// The product's title. + @JsonKey(defaultValue: '') final String title; /// The [SkuType] of the product. final SkuType type; /// False if the product is paid. + @JsonKey(defaultValue: false) final bool isRewarded; /// The original price that the user purchased this product for. + @JsonKey(defaultValue: '') final String originalPrice; /// [originalPrice] in micro-units ("990000"). + @JsonKey(defaultValue: 0) final int originalPriceAmountMicros; @override @@ -150,7 +172,7 @@ class SkuDetailsResponseWrapper { /// Creates a [SkuDetailsResponseWrapper] with the given purchase details. @visibleForTesting SkuDetailsResponseWrapper( - {@required this.billingResult, this.skuDetailsList}); + {required this.billingResult, required this.skuDetailsList}); /// Constructs an instance of this from a key value map of data. /// @@ -163,6 +185,7 @@ class SkuDetailsResponseWrapper { final BillingResultWrapper billingResult; /// A list of [SkuDetailsWrapper] matching the query to [BillingClient.querySkuDetails]. + @JsonKey(defaultValue: []) final List skuDetailsList; @override @@ -186,22 +209,29 @@ class SkuDetailsResponseWrapper { @BillingResponseConverter() class BillingResultWrapper { /// Constructs the object with [responseCode] and [debugMessage]. - BillingResultWrapper({@required this.responseCode, this.debugMessage}); + BillingResultWrapper({required this.responseCode, this.debugMessage}); /// Constructs an instance of this from a key value map of data. /// /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. - factory BillingResultWrapper.fromJson(Map map) => - _$BillingResultWrapperFromJson(map); + factory BillingResultWrapper.fromJson(Map? map) { + if (map == null || map.isEmpty) { + return BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage); + } + return _$BillingResultWrapperFromJson(map); + } /// Response code returned in the Play Billing API calls. final BillingResponse responseCode; /// Debug message returned in the Play Billing API calls. /// + /// Defaults to `null`. /// This message uses an en-US locale and should not be shown to users. - final String debugMessage; + final String? debugMessage; @override bool operator ==(dynamic other) { diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 70bde9318f03..247dbd54b666 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -8,22 +8,22 @@ part of 'sku_details_wrapper.dart'; SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { return SkuDetailsWrapper( - description: json['description'] as String, - freeTrialPeriod: json['freeTrialPeriod'] as String, - introductoryPrice: json['introductoryPrice'] as String, - introductoryPriceMicros: json['introductoryPriceMicros'] as String, - introductoryPriceCycles: json['introductoryPriceCycles'] as String, - introductoryPricePeriod: json['introductoryPricePeriod'] as String, - price: json['price'] as String, - priceAmountMicros: json['priceAmountMicros'] as int, - priceCurrencyCode: json['priceCurrencyCode'] as String, - sku: json['sku'] as String, - subscriptionPeriod: json['subscriptionPeriod'] as String, - title: json['title'] as String, - type: const SkuTypeConverter().fromJson(json['type'] as String), - isRewarded: json['isRewarded'] as bool, - originalPrice: json['originalPrice'] as String, - originalPriceAmountMicros: json['originalPriceAmountMicros'] as int, + description: json['description'] as String? ?? '', + freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', + introductoryPrice: json['introductoryPrice'] as String? ?? '', + introductoryPriceMicros: json['introductoryPriceMicros'] as String? ?? '', + introductoryPriceCycles: json['introductoryPriceCycles'] as String? ?? '', + introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', + price: json['price'] as String? ?? '', + priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, + priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', + sku: json['sku'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', + title: json['title'] as String? ?? '', + type: const SkuTypeConverter().fromJson(json['type'] as String?), + isRewarded: json['isRewarded'] as bool? ?? false, + originalPrice: json['originalPrice'] as String? ?? '', + originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, ); } @@ -49,10 +49,15 @@ Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { return SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - skuDetailsList: (json['skuDetailsList'] as List) - .map((e) => SkuDetailsWrapper.fromJson(e as Map)) - .toList(), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + skuDetailsList: (json['skuDetailsList'] as List?) + ?.map((e) => + SkuDetailsWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], ); } @@ -66,8 +71,8 @@ Map _$SkuDetailsResponseWrapperToJson( BillingResultWrapper _$BillingResultWrapperFromJson(Map json) { return BillingResultWrapper( responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int), - debugMessage: json['debugMessage'] as String, + const BillingResponseConverter().fromJson(json['responseCode'] as int?), + debugMessage: json['debugMessage'] as String?, ); } diff --git a/packages/in_app_purchase/lib/src/channel.dart b/packages/in_app_purchase/lib/src/channel.dart index a0b92b5d5f1e..5d140e281e7b 100644 --- a/packages/in_app_purchase/lib/src/channel.dart +++ b/packages/in_app_purchase/lib/src/channel.dart @@ -4,13 +4,6 @@ import 'package:flutter/services.dart'; -/// Method channel for the plugin's platform<-->Dart calls (all but the -/// ios->Dart calls which are carried over the [callbackChannel]). +/// Method channel for the plugin's platform<-->Dart calls. const MethodChannel channel = MethodChannel('plugins.flutter.io/in_app_purchase'); - -/// Method channel for the plugin's ios->Dart calls. -// This is in a separate channel due to historic reasons only. -// TODO(cyanglaz): Remove this. https://github.com/flutter/flutter/issues/69225 -const MethodChannel callbackChannel = - MethodChannel('plugins.flutter.io/in_app_purchase_callback'); diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart index a244ab13fc28..50560a666a40 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart @@ -21,9 +21,9 @@ class AppStoreConnection implements InAppPurchaseConnection { /// Returns the singleton instance of the [AppStoreConnection] that should be /// used across the app. static AppStoreConnection get instance => _getOrCreateInstance(); - static AppStoreConnection _instance; - static SKPaymentQueueWrapper _skPaymentQueueWrapper; - static _TransactionObserver _observer; + static AppStoreConnection? _instance; + static late SKPaymentQueueWrapper _skPaymentQueueWrapper; + static late _TransactionObserver _observer; /// Creates an [AppStoreConnection] object. /// @@ -41,55 +41,61 @@ class AppStoreConnection implements InAppPurchaseConnection { static AppStoreConnection _getOrCreateInstance() { if (_instance != null) { - return _instance; + return _instance!; } _instance = AppStoreConnection(); _skPaymentQueueWrapper = SKPaymentQueueWrapper(); _observer = _TransactionObserver(StreamController.broadcast()); _skPaymentQueueWrapper.setTransactionObserver(observer); - return _instance; + return _instance!; } @override Future isAvailable() => SKPaymentQueueWrapper.canMakePayments(); @override - Future buyNonConsumable({@required PurchaseParam purchaseParam}) async { + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( productIdentifier: purchaseParam.productDetails.id, quantity: 1, applicationUsername: purchaseParam.applicationUserName, - simulatesAskToBuyInSandbox: purchaseParam.sandboxTesting, + simulatesAskToBuyInSandbox: purchaseParam.simulatesAskToBuyInSandbox || + // ignore: deprecated_member_use_from_same_package + purchaseParam.sandboxTesting, requestData: null)); return true; // There's no error feedback from iOS here to return. } @override Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}) { + {required PurchaseParam purchaseParam, bool autoConsume = true}) { assert(autoConsume == true, 'On iOS, we should always auto consume'); return buyNonConsumable(purchaseParam: purchaseParam); } @override Future completePurchase(PurchaseDetails purchase, - {String developerPayload}) async { + {String? developerPayload}) async { + if (purchase.skPaymentTransaction == null) { + throw ArgumentError( + 'completePurchase unsuccessful. The `purchase.skPaymentTransaction` is not valid'); + } await _skPaymentQueueWrapper - .finishTransaction(purchase.skPaymentTransaction); + .finishTransaction(purchase.skPaymentTransaction!); return BillingResultWrapper(responseCode: BillingResponse.ok); } @override Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}) { + {String? developerPayload}) { throw UnsupportedError('consume purchase is not available on Android'); } @override Future queryPastPurchases( - {String applicationUserName}) async { - IAPError error; + {String? applicationUserName}) async { + IAPError? error; List pastPurchases = []; try { @@ -98,7 +104,6 @@ class AppStoreConnection implements InAppPurchaseConnection { await _observer.getRestoredTransactions( queue: _skPaymentQueueWrapper, applicationUserName: applicationUserName); - _observer.cleanUpRestoredTransactions(); pastPurchases = restoredTransactions.map((SKPaymentTransactionWrapper transaction) { assert(transaction.transactionState == @@ -110,16 +115,17 @@ class AppStoreConnection implements InAppPurchaseConnection { ? IAPError( source: IAPSource.AppStore, code: kPurchaseErrorCode, - message: transaction.error.domain, - details: transaction.error.userInfo, + message: transaction.error?.domain ?? '', + details: transaction.error?.userInfo, ) : null; }).toList(); + _observer.cleanUpRestoredTransactions(); } on PlatformException catch (e) { error = IAPError( source: IAPSource.AppStore, code: e.code, - message: e.message, + message: e.message ?? '', details: e.details); } on SKError catch (e) { error = IAPError( @@ -133,9 +139,12 @@ class AppStoreConnection implements InAppPurchaseConnection { } @override - Future refreshPurchaseVerificationData() async { + Future refreshPurchaseVerificationData() async { await SKRequestMaker().startRefreshReceiptRequest(); - String receipt = await SKReceiptManager.retrieveReceiptData(); + final String? receipt = await SKReceiptManager.retrieveReceiptData(); + if (receipt == null) { + return null; + } return PurchaseVerificationData( localVerificationData: receipt, serverVerificationData: receipt, @@ -152,7 +161,7 @@ class AppStoreConnection implements InAppPurchaseConnection { Set identifiers) async { final SKRequestMaker requestMaker = SKRequestMaker(); SkProductResponseWrapper response; - PlatformException exception; + PlatformException? exception; try { response = await requestMaker.startProductRequest(identifiers.toList()); } on PlatformException catch (e) { @@ -167,7 +176,7 @@ class AppStoreConnection implements InAppPurchaseConnection { ProductDetails.fromSKProduct(productWrapper)) .toList(); } - List invalidIdentifiers = response.invalidProductIdentifiers ?? []; + List invalidIdentifiers = response.invalidProductIdentifiers; if (productDetails.isEmpty) { invalidIdentifiers = identifiers.toList(); } @@ -179,7 +188,7 @@ class AppStoreConnection implements InAppPurchaseConnection { : IAPError( source: IAPSource.AppStore, code: exception.code, - message: exception.message, + message: exception.message ?? '', details: exception.details), ); return productDetailsResponse; @@ -189,27 +198,27 @@ class AppStoreConnection implements InAppPurchaseConnection { class _TransactionObserver implements SKTransactionObserverWrapper { final StreamController> purchaseUpdatedController; - Completer> _restoreCompleter; - List _restoredTransactions; - String _receiptData; + Completer>? _restoreCompleter; + List _restoredTransactions = + []; + late String _receiptData; _TransactionObserver(this.purchaseUpdatedController); Future> getRestoredTransactions( - {@required SKPaymentQueueWrapper queue, String applicationUserName}) { - assert(queue != null); + {required SKPaymentQueueWrapper queue, String? applicationUserName}) { _restoreCompleter = Completer(); queue.restoreTransactions(applicationUserName: applicationUserName); - return _restoreCompleter.future; + return _restoreCompleter!.future; } void cleanUpRestoredTransactions() { - _restoredTransactions = null; + _restoredTransactions.clear(); _restoreCompleter = null; } void updatedTransactions( - {List transactions}) async { + {required List transactions}) async { if (_restoreCompleter != null) { if (_restoredTransactions == null) { _restoredTransactions = []; @@ -233,19 +242,20 @@ class _TransactionObserver implements SKTransactionObserverWrapper { }).toList()); } - void removedTransactions({List transactions}) {} + void removedTransactions( + {required List transactions}) {} /// Triggered when there is an error while restoring transactions. - void restoreCompletedTransactionsFailed({SKError error}) { - _restoreCompleter.completeError(error); + void restoreCompletedTransactionsFailed({required SKError error}) { + _restoreCompleter!.completeError(error); } void paymentQueueRestoreCompletedTransactionsFinished() { - _restoreCompleter.complete(_restoredTransactions ?? []); + _restoreCompleter!.complete(_restoredTransactions); } bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}) { + {required SKPaymentWrapper payment, required SKProductWrapper product}) { // In this unified API, we always return true to keep it consistent with the behavior on Google Play. return true; } @@ -254,7 +264,7 @@ class _TransactionObserver implements SKTransactionObserverWrapper { try { _receiptData = await SKReceiptManager.retrieveReceiptData(); } catch (e) { - _receiptData = null; + _receiptData = ''; } return _receiptData; } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart index b980bbd77d85..ef0b7d2efa59 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; import '../../billing_client_wrappers.dart'; +import '../../in_app_purchase.dart'; import 'in_app_purchase_connection.dart'; import 'product_details.dart'; @@ -28,26 +29,27 @@ class GooglePlayConnection billingClient.enablePendingPurchases(); } _readyFuture = _connect(); - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance!.addObserver(this); _purchaseUpdatedController = StreamController.broadcast(); ; } /// Returns the singleton instance of the [GooglePlayConnection]. static GooglePlayConnection get instance => _getOrCreateInstance(); - static GooglePlayConnection _instance; + static GooglePlayConnection? _instance; Stream> get purchaseUpdatedStream => _purchaseUpdatedController.stream; - static StreamController> _purchaseUpdatedController; + static late StreamController> + _purchaseUpdatedController; /// The [BillingClient] that's abstracted by [GooglePlayConnection]. /// /// This field should not be used out of test code. @visibleForTesting - final BillingClient billingClient; + late final BillingClient billingClient; - Future _readyFuture; + late Future _readyFuture; static Set _productIdsToConsume = Set(); @override @@ -57,7 +59,7 @@ class GooglePlayConnection } @override - Future buyNonConsumable({@required PurchaseParam purchaseParam}) async { + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { BillingResultWrapper billingResultWrapper = await billingClient.launchBillingFlow( sku: purchaseParam.productDetails.id, @@ -67,7 +69,7 @@ class GooglePlayConnection @override Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}) { + {required PurchaseParam purchaseParam, bool autoConsume = true}) { if (autoConsume) { _productIdsToConsume.add(purchaseParam.productDetails.id); } @@ -76,10 +78,14 @@ class GooglePlayConnection @override Future completePurchase(PurchaseDetails purchase, - {String developerPayload}) async { - if (purchase.billingClientPurchase.isAcknowledged) { + {String? developerPayload}) async { + if (purchase.billingClientPurchase!.isAcknowledged) { return BillingResultWrapper(responseCode: BillingResponse.ok); } + if (purchase.verificationData == null) { + throw ArgumentError( + 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); + } return await billingClient.acknowledgePurchase( purchase.verificationData.serverVerificationData, developerPayload: developerPayload); @@ -87,7 +93,11 @@ class GooglePlayConnection @override Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}) { + {String? developerPayload}) { + if (purchase.verificationData == null) { + throw ArgumentError( + 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); + } return billingClient.consumeAsync( purchase.verificationData.serverVerificationData, developerPayload: developerPayload); @@ -95,9 +105,9 @@ class GooglePlayConnection @override Future queryPastPurchases( - {String applicationUserName}) async { + {String? applicationUserName}) async { List responses; - PlatformException exception; + PlatformException? exception; try { responses = await Future.wait([ billingClient.queryPurchases(SkuType.inapp), @@ -133,7 +143,7 @@ class GooglePlayConnection .toSet(); String errorMessage = - errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : null; + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; List pastPurchases = responses.expand((PurchasesResultWrapper response) { @@ -142,14 +152,14 @@ class GooglePlayConnection return PurchaseDetails.fromPurchase(purchaseWrapper); }).toList(); - IAPError error; + IAPError? error; if (exception != null) { error = IAPError( source: IAPSource.GooglePlay, code: exception.code, - message: exception.message, + message: exception.message ?? '', details: exception.details); - } else if (errorMessage != null) { + } else if (errorMessage.isNotEmpty) { error = IAPError( source: IAPSource.GooglePlay, code: kRestoredPurchaseErrorCode, @@ -175,11 +185,11 @@ class GooglePlayConnection static GooglePlayConnection _getOrCreateInstance() { if (_instance != null) { - return _instance; + return _instance!; } _instance = GooglePlayConnection._(); - return _instance; + return _instance!; } Future _connect() => @@ -193,7 +203,7 @@ class GooglePlayConnection Future queryProductDetails( Set identifiers) async { List responses; - PlatformException exception; + PlatformException? exception; try { responses = await Future.wait([ billingClient.querySkuDetails( @@ -235,13 +245,13 @@ class GooglePlayConnection : IAPError( source: IAPSource.GooglePlay, code: exception.code, - message: exception.message, + message: exception.message ?? '', details: exception.details)); } static Future> _getPurchaseDetailsFromResult( PurchasesResultWrapper resultWrapper) async { - IAPError error; + IAPError? error; if (resultWrapper.responseCode != BillingResponse.ok) { error = IAPError( source: IAPSource.GooglePlay, @@ -260,10 +270,13 @@ class GooglePlayConnection } else { return [ PurchaseDetails( - purchaseID: null, - productID: null, + purchaseID: '', + productID: '', transactionDate: null, - verificationData: null) + verificationData: PurchaseVerificationData( + localVerificationData: '', + serverVerificationData: '', + source: IAPSource.GooglePlay)) ..status = PurchaseStatus.error ..error = error ]; diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart index f07ff96d4403..81a0e92cc591 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart @@ -7,7 +7,6 @@ import 'dart:io'; import 'app_store_connection.dart'; import 'google_play_connection.dart'; import 'product_details.dart'; -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/billing_client_wrappers.dart'; import './purchase_details.dart'; @@ -40,11 +39,11 @@ abstract class InAppPurchaseConnection { /// events after they start to listen. Stream> get purchaseUpdatedStream => _getStream(); - Stream> _purchaseUpdatedStream; + Stream>? _purchaseUpdatedStream; Stream> _getStream() { if (_purchaseUpdatedStream != null) { - return _purchaseUpdatedStream; + return _purchaseUpdatedStream!; } if (Platform.isAndroid) { @@ -57,7 +56,7 @@ abstract class InAppPurchaseConnection { throw UnsupportedError( 'InAppPurchase plugin only works on Android and iOS.'); } - return _purchaseUpdatedStream; + return _purchaseUpdatedStream!; } /// Whether pending purchase is enabled. @@ -133,7 +132,7 @@ abstract class InAppPurchaseConnection { /// * [queryPastPurchases], for restoring non consumable products. /// /// Calling this method for consumable items will cause unwanted behaviors! - Future buyNonConsumable({@required PurchaseParam purchaseParam}); + Future buyNonConsumable({required PurchaseParam purchaseParam}); /// Buy a consumable product. /// @@ -186,7 +185,7 @@ abstract class InAppPurchaseConnection { /// Calling this method for non consumable items will cause unwanted /// behaviors! Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}); + {required PurchaseParam purchaseParam, bool autoConsume = true}); /// Mark that purchased content has been delivered to the /// user. @@ -206,9 +205,9 @@ abstract class InAppPurchaseConnection { /// Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android. /// The [consumePurchase] acts as an implicit [completePurchase] on Android. /// - /// The optional parameter `developerPayload` only works on Android. + /// The optional parameter `developerPayload` (defaults to `null`) only works on Android. Future completePurchase(PurchaseDetails purchase, - {String developerPayload}); + {String? developerPayload}); /// (Play only) Mark that the user has consumed a product. /// @@ -216,16 +215,17 @@ abstract class InAppPurchaseConnection { /// delivered. The user won't be able to buy the same product again until the /// purchase of the product is consumed. /// - /// The `developerPayload` can be specified to be associated with this consumption. + /// The `developerPayload` (defaults to `null`) can be specified to be associated with this consumption. /// /// This throws an [UnsupportedError] on iOS. Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}); + {String? developerPayload}); /// Query all previous purchases. /// /// The `applicationUserName` should match whatever was sent in the initial - /// `PurchaseParam`, if anything. + /// `PurchaseParam`, if anything. If no `applicationUserName` was specified in the initial + /// `PurchaseParam`, use `null`. /// /// This does not return consumed products. If you want to restore unused /// consumable products, you need to persist consumable product information @@ -236,23 +236,25 @@ abstract class InAppPurchaseConnection { /// * [refreshPurchaseVerificationData], for reloading failed /// [PurchaseDetails.verificationData]. Future queryPastPurchases( - {String applicationUserName}); + {String? applicationUserName}); /// (App Store only) retry loading purchase data after an initial failure. /// + /// If no results, a `null` value is returned. + /// /// Throws an [UnsupportedError] on Android. - Future refreshPurchaseVerificationData(); + Future refreshPurchaseVerificationData(); /// The [InAppPurchaseConnection] implemented for this platform. /// /// Throws an [UnsupportedError] when accessed on a platform other than /// Android or iOS. static InAppPurchaseConnection get instance => _getOrCreateInstance(); - static InAppPurchaseConnection _instance; + static InAppPurchaseConnection? _instance; static InAppPurchaseConnection _getOrCreateInstance() { if (_instance != null) { - return _instance; + return _instance!; } if (Platform.isAndroid) { @@ -264,7 +266,7 @@ abstract class InAppPurchaseConnection { 'InAppPurchase plugin only works on Android and iOS.'); } - return _instance; + return _instance!; } } @@ -287,9 +289,9 @@ enum IAPSource { class IAPError { /// Creates a new IAP error object with the given error details. IAPError( - {@required this.source, - @required this.code, - @required this.message, + {required this.source, + required this.code, + required this.message, this.details}); /// Which source is the error on. @@ -298,9 +300,9 @@ class IAPError { /// The error code. final String code; - /// A human-readable error message, possibly null. + /// A human-readable error message. final String message; /// Error details, possibly null. - final dynamic details; + final dynamic? details; } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart index bb9e2682b9b7..a3eb79d9a450 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/store_kit_wrappers.dart'; import 'package:in_app_purchase/billing_client_wrappers.dart'; import 'in_app_purchase_connection.dart'; @@ -14,10 +13,10 @@ import 'in_app_purchase_connection.dart'; class ProductDetails { /// Creates a new product details object with the provided details. ProductDetails( - {@required this.id, - @required this.title, - @required this.description, - @required this.price, + {required this.id, + required this.title, + required this.description, + required this.price, this.skProduct, this.skuDetail}); @@ -36,13 +35,13 @@ class ProductDetails { /// Points back to the `StoreKits`'s [SKProductWrapper] object that generated this [ProductDetails] object. /// - /// This is null on Android. - final SKProductWrapper skProduct; + /// This is `null` on Android. + final SKProductWrapper? skProduct; /// Points back to the `BillingClient1`'s [SkuDetailsWrapper] object that generated this [ProductDetails] object. /// - /// This is null on iOS. - final SkuDetailsWrapper skuDetail; + /// This is `null` on iOS. + final SkuDetailsWrapper? skuDetail; /// Generate a [ProductDetails] object based on an iOS [SKProductWrapper] object. ProductDetails.fromSKProduct(SKProductWrapper product) @@ -69,7 +68,7 @@ class ProductDetails { class ProductDetailsResponse { /// Creates a new [ProductDetailsResponse] with the provided response details. ProductDetailsResponse( - {@required this.productDetails, @required this.notFoundIDs, this.error}); + {required this.productDetails, required this.notFoundIDs, this.error}); /// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchaseConnection.queryProductDetails]. final List productDetails; @@ -82,7 +81,9 @@ class ProductDetailsResponse { /// A caught platform exception thrown while querying the purchases. /// + /// The value is `null` if there is no error. + /// /// It's possible for this to be null but for there still to be notFoundIds in cases where the request itself was a success but the /// requested IDs could not be found. - final IAPError error; + final IAPError? error; } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart index 708b42c01623..c211d2a4cdb8 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/purchase_wrapper.dart'; import 'package:in_app_purchase/src/store_kit_wrappers/enum_converters.dart'; @@ -59,9 +58,9 @@ class PurchaseVerificationData { /// Creates a [PurchaseVerificationData] object with the provided information. PurchaseVerificationData( - {@required this.localVerificationData, - @required this.serverVerificationData, - @required this.source}); + {required this.localVerificationData, + required this.serverVerificationData, + required this.source}); } /// Status for a [PurchaseDetails]. @@ -88,9 +87,10 @@ enum PurchaseStatus { class PurchaseParam { /// Creates a new purchase parameter object with the given data. PurchaseParam( - {@required this.productDetails, + {required this.productDetails, this.applicationUserName, - this.sandboxTesting}); + this.sandboxTesting = false, + this.simulatesAskToBuyInSandbox = false}); /// The product to create payment for. /// @@ -103,10 +103,20 @@ class PurchaseParam { /// Do not pass in a clear text, your developer ID, the user’s Apple ID, or the /// user's Google ID for this field. /// For example, you can use a one-way hash of the user’s account name on your server. - final String applicationUserName; + final String? applicationUserName; - /// The 'sandboxTesting' is only available on iOS, set it to `true` for testing in AppStore's sandbox environment. The default value is `false`. + /// @deprecated Use [simulatesAskToBuyInSandbox] instead. + /// + /// Only available on iOS, set it to `true` to produce an "ask to buy" flow for this payment in the sandbox. + /// + /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. + @deprecated final bool sandboxTesting; + + /// Only available on iOS, set it to `true` to produce an "ask to buy" flow for this payment in the sandbox. + /// + /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. + final bool simulatesAskToBuyInSandbox; } /// Represents the transaction details of a purchase. @@ -115,7 +125,9 @@ class PurchaseParam { /// This class for simple operations. If you would like to see the detailed representation of the product, instead, use [PurchaseWrapper] on Android and [SKPaymentTransactionWrapper] on iOS. class PurchaseDetails { /// A unique identifier of the purchase. - final String purchaseID; + /// + /// The `value` is null on iOS if it is not a successful purchase. + final String? purchaseID; /// The product identifier of the purchase. final String productID; @@ -126,15 +138,16 @@ class PurchaseDetails { /// details on how to verify purchase use this data. You should never use any /// purchase data until verified. /// - /// On iOS, this may be null. Call - /// [InAppPurchaseConnection.refreshPurchaseVerificationData] to get a new + /// On iOS, [InAppPurchaseConnection.refreshPurchaseVerificationData] can be used to get a new /// [PurchaseVerificationData] object for further validation. final PurchaseVerificationData verificationData; /// The timestamp of the transaction. /// /// Milliseconds since epoch. - final String transactionDate; + /// + /// The value is `null` if [status] is not [PurchaseStatus.purchased]. + final String? transactionDate; /// The status that this [PurchaseDetails] is currently on. PurchaseStatus get status => _status; @@ -153,20 +166,22 @@ class PurchaseDetails { _status = status; } - PurchaseStatus _status; + late PurchaseStatus _status; - /// The error is only available when [status] is [PurchaseStatus.error]. - IAPError error; + /// The error details when the [status] is [PurchaseStatus.error]. + /// + /// The value is `null` if [status] is not [PurchaseStatus.error]. + IAPError? error; /// Points back to the `StoreKits`'s [SKPaymentTransactionWrapper] object that generated this [PurchaseDetails] object. /// - /// This is null on Android. - final SKPaymentTransactionWrapper skPaymentTransaction; + /// This is `null` on Android. + final SKPaymentTransactionWrapper? skPaymentTransaction; /// Points back to the `BillingClient`'s [PurchaseWrapper] object that generated this [PurchaseDetails] object. /// - /// This is null on iOS. - final PurchaseWrapper billingClientPurchase; + /// This is `null` on iOS. + final PurchaseWrapper? billingClientPurchase; /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true` /// and the product has been delivered to the user. @@ -179,14 +194,14 @@ class PurchaseDetails { // The platform that the object is created on. // // The value is either '_kPlatformIOS' or '_kPlatformAndroid'. - String _platform; + String? _platform; /// Creates a new PurchaseDetails object with the provided data. PurchaseDetails({ - @required this.purchaseID, - @required this.productID, - @required this.verificationData, - @required this.transactionDate, + this.purchaseID, + required this.productID, + required this.verificationData, + required this.transactionDate, this.skPaymentTransaction, this.billingClientPurchase, }); @@ -201,7 +216,7 @@ class PurchaseDetails { serverVerificationData: base64EncodedReceipt, source: IAPSource.AppStore), this.transactionDate = transaction.transactionTimeStamp != null - ? (transaction.transactionTimeStamp * 1000).toInt().toString() + ? (transaction.transactionTimeStamp! * 1000).toInt().toString() : null, this.skPaymentTransaction = transaction, this.billingClientPurchase = null, @@ -212,8 +227,8 @@ class PurchaseDetails { error = IAPError( source: IAPSource.AppStore, code: kPurchaseErrorCode, - message: transaction.error.domain, - details: transaction.error.userInfo, + message: transaction.error?.domain ?? '', + details: transaction.error?.userInfo, ); } } @@ -235,7 +250,7 @@ class PurchaseDetails { error = IAPError( source: IAPSource.GooglePlay, code: kPurchaseErrorCode, - message: null, + message: '', ); } } @@ -246,7 +261,7 @@ class PurchaseDetails { /// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases]. class QueryPurchaseDetailsResponse { /// Creates a new [QueryPurchaseDetailsResponse] object with the provider information. - QueryPurchaseDetailsResponse({@required this.pastPurchases, this.error}); + QueryPurchaseDetailsResponse({required this.pastPurchases, this.error}); /// A list of successfully fetched past purchases. /// @@ -257,6 +272,6 @@ class QueryPurchaseDetailsResponse { /// The error when fetching past purchases. /// - /// If the fetch is successful, the value is null. - final IAPError error; + /// If the fetch is successful, the value is `null`. + final IAPError? error; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart index 62188705035a..ce2c1fad406f 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart @@ -13,16 +13,20 @@ part 'enum_converters.g.dart'; /// Use these in `@JsonSerializable()` classes by annotating them with /// `@SKTransactionStatusConverter()`. class SKTransactionStatusConverter - implements JsonConverter { + implements JsonConverter { /// Default const constructor. const SKTransactionStatusConverter(); @override - SKPaymentTransactionStateWrapper fromJson(int json) => - _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap - .cast(), - json); + SKPaymentTransactionStateWrapper fromJson(int? json) { + if (json == null) { + return SKPaymentTransactionStateWrapper.unspecified; + } + return _$enumDecode( + _$SKPaymentTransactionStateWrapperEnumMap + .cast(), + json); + } /// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus]. PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) { @@ -34,19 +38,70 @@ class SKTransactionStatusConverter case SKPaymentTransactionStateWrapper.restored: return PurchaseStatus.purchased; case SKPaymentTransactionStateWrapper.failed: + case SKPaymentTransactionStateWrapper.unspecified: return PurchaseStatus.error; } - - throw ArgumentError('$object isn\'t mapped to PurchaseStatus'); } @override int toJson(SKPaymentTransactionStateWrapper object) => - _$SKPaymentTransactionStateWrapperEnumMap[object]; + _$SKPaymentTransactionStateWrapperEnumMap[object]!; +} + +/// Serializer for [SKSubscriptionPeriodUnit]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKSubscriptionPeriodUnitConverter()`. +class SKSubscriptionPeriodUnitConverter + implements JsonConverter { + /// Default const constructor. + const SKSubscriptionPeriodUnitConverter(); + + @override + SKSubscriptionPeriodUnit fromJson(int? json) { + if (json == null) { + return SKSubscriptionPeriodUnit.day; + } + return _$enumDecode( + _$SKSubscriptionPeriodUnitEnumMap + .cast(), + json); + } + + @override + int toJson(SKSubscriptionPeriodUnit object) => + _$SKSubscriptionPeriodUnitEnumMap[object]!; +} + +/// Serializer for [SKProductDiscountPaymentMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKProductDiscountPaymentModeConverter()`. +class SKProductDiscountPaymentModeConverter + implements JsonConverter { + /// Default const constructor. + const SKProductDiscountPaymentModeConverter(); + + @override + SKProductDiscountPaymentMode fromJson(int? json) { + if (json == null) { + return SKProductDiscountPaymentMode.payAsYouGo; + } + return _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap + .cast(), + json); + } + + @override + int toJson(SKProductDiscountPaymentMode object) => + _$SKProductDiscountPaymentModeEnumMap[object]!; } // Define a class so we generate serializer helper methods for the enums @JsonSerializable() class _SerializedEnums { - SKPaymentTransactionStateWrapper response; + late SKPaymentTransactionStateWrapper response; + late SKSubscriptionPeriodUnit unit; + late SKProductDiscountPaymentMode discountPaymentMode; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart index f4f17df846a7..b003f435a800 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -9,33 +9,44 @@ part of 'enum_converters.dart'; _SerializedEnums _$_SerializedEnumsFromJson(Map json) { return _SerializedEnums() ..response = _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap, json['response']); + _$SKPaymentTransactionStateWrapperEnumMap, json['response']) + ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) + ..discountPaymentMode = _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); } Map _$_SerializedEnumsToJson(_SerializedEnums instance) => { 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], + 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], + 'discountPaymentMode': + _$SKProductDiscountPaymentModeEnumMap[instance.discountPaymentMode], }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, }) { if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); } - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; } const _$SKPaymentTransactionStateWrapperEnumMap = { @@ -44,4 +55,19 @@ const _$SKPaymentTransactionStateWrapperEnumMap = { SKPaymentTransactionStateWrapper.failed: 2, SKPaymentTransactionStateWrapper.restored: 3, SKPaymentTransactionStateWrapper.deferred: 4, + SKPaymentTransactionStateWrapper.unspecified: -1, +}; + +const _$SKSubscriptionPeriodUnitEnumMap = { + SKSubscriptionPeriodUnit.day: 0, + SKSubscriptionPeriodUnit.week: 1, + SKSubscriptionPeriodUnit.month: 2, + SKSubscriptionPeriodUnit.year: 3, +}; + +const _$SKProductDiscountPaymentModeEnumMap = { + SKProductDiscountPaymentMode.payAsYouGo: 0, + SKProductDiscountPaymentMode.payUpFront: 1, + SKProductDiscountPaymentMode.freeTrail: 2, + SKProductDiscountPaymentMode.unspecified: -1, }; diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index ce38759c74ec..d56fbd00c6fe 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -5,7 +5,6 @@ import 'dart:ui' show hashValues; import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/src/channel.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:flutter/services.dart'; @@ -25,7 +24,7 @@ part 'sk_payment_queue_wrapper.g.dart'; /// available at the [In-App Purchase Programming /// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). class SKPaymentQueueWrapper { - SKTransactionObserverWrapper _observer; + SKTransactionObserverWrapper? _observer; /// Returns the default payment queue. /// @@ -41,13 +40,15 @@ class SKPaymentQueueWrapper { /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) Future> transactions() async { - return _getTransactionList( - await channel.invokeListMethod('-[SKPaymentQueue transactions]')); + return _getTransactionList((await channel + .invokeListMethod('-[SKPaymentQueue transactions]'))!); } /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). static Future canMakePayments() async => - await channel.invokeMethod('-[SKPaymentQueue canMakePayments:]'); + (await channel + .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? + false; /// Sets an observer to listen to all incoming transaction events. /// @@ -57,7 +58,7 @@ class SKPaymentQueueWrapper { /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). void setTransactionObserver(SKTransactionObserverWrapper observer) { _observer = observer; - callbackChannel.setMethodCallHandler(_handleObserverCallbacks); + channel.setMethodCallHandler(_handleObserverCallbacks); } /// Posts a payment to the queue. @@ -83,7 +84,7 @@ class SKPaymentQueueWrapper { Future addPayment(SKPaymentWrapper payment) async { assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); - Map requestMap = payment.toMap(); + final Map requestMap = payment.toMap(); await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', requestMap, @@ -103,7 +104,7 @@ class SKPaymentQueueWrapper { /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). Future finishTransaction( SKPaymentTransactionWrapper transaction) async { - Map requestMap = transaction.toFinishMap(); + Map requestMap = transaction.toFinishMap(); await channel.invokeMethod( '-[InAppPurchasePlugin finishTransaction:result:]', requestMap, @@ -124,28 +125,30 @@ class SKPaymentQueueWrapper { /// /// The `applicationUserName` should match the original /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. + /// If no `applicationUserName` was used, `applicationUserName` should be null. /// /// This method either triggers [`-[SKPayment /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) /// depending on whether the `applicationUserName` is set. - Future restoreTransactions({String applicationUserName}) async { + Future restoreTransactions({String? applicationUserName}) async { await channel.invokeMethod( '-[InAppPurchasePlugin restoreTransactions:result:]', applicationUserName); } // Triage a method channel call from the platform and triggers the correct observer method. - Future _handleObserverCallbacks(MethodCall call) { + Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); + final SKTransactionObserverWrapper observer = _observer!; switch (call.method) { case 'updatedTransactions': { final List transactions = _getTransactionList(call.arguments); return Future(() { - _observer.updatedTransactions(transactions: transactions); + observer.updatedTransactions(transactions: transactions); }); } case 'removedTransactions': @@ -153,20 +156,20 @@ class SKPaymentQueueWrapper { final List transactions = _getTransactionList(call.arguments); return Future(() { - _observer.removedTransactions(transactions: transactions); + observer.removedTransactions(transactions: transactions); }); } case 'restoreCompletedTransactionsFailed': { SKError error = SKError.fromJson(call.arguments); return Future(() { - _observer.restoreCompletedTransactionsFailed(error: error); + observer.restoreCompletedTransactionsFailed(error: error); }); } case 'paymentQueueRestoreCompletedTransactionsFinished': { return Future(() { - _observer.paymentQueueRestoreCompletedTransactionsFinished(); + observer.paymentQueueRestoreCompletedTransactionsFinished(); }); } case 'shouldAddStorePayment': @@ -176,7 +179,7 @@ class SKPaymentQueueWrapper { SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); return Future(() { - if (_observer.shouldAddStorePayment( + if (observer.shouldAddStorePayment( payment: payment, product: product) == true) { SKPaymentQueueWrapper().addPayment(payment); @@ -186,49 +189,52 @@ class SKPaymentQueueWrapper { default: break; } - return null; + throw PlatformException( + code: 'no_such_callback', + message: 'Did not recognize the observer callback ${call.method}.'); } // Get transaction wrapper object list from arguments. - List _getTransactionList(dynamic arguments) { - final List transactions = arguments - .map( - (dynamic map) => SKPaymentTransactionWrapper.fromJson(map)) - .toList(); - return transactions; + List _getTransactionList( + List transactionsData) { + return transactionsData.map((dynamic map) { + return SKPaymentTransactionWrapper.fromJson( + Map.castFrom(map)); + }).toList(); } } /// Dart wrapper around StoreKit's /// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). -@JsonSerializable(nullable: true) +@JsonSerializable() class SKError { /// Creates a new [SKError] object with the provided information. - SKError( - {@required this.code, @required this.domain, @required this.userInfo}); + SKError({required this.code, required this.domain, required this.userInfo}); /// Constructs an instance of this from a key-value map of data. /// /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKError.fromJson(Map map) { - assert(map != null); + factory SKError.fromJson(Map map) { return _$SKErrorFromJson(map); } /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: 0) final int code; /// Error /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: '') final String domain; /// A map that contains more detailed information about the error. /// /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). + @JsonKey(defaultValue: {}) final Map userInfo; @override @@ -239,7 +245,7 @@ class SKError { if (other.runtimeType != runtimeType) { return false; } - final SKError typedOther = other; + final SKError typedOther = other as SKError; return typedOther.code == code && typedOther.domain == domain && DeepCollectionEquality.unordered() @@ -257,11 +263,11 @@ class SKError { /// not need to create the payment object explicitly; instead, use /// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to /// initiate a payment. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKPaymentWrapper { /// Creates a new [SKPaymentWrapper] with the provided information. SKPaymentWrapper( - {@required this.productIdentifier, + {required this.productIdentifier, this.applicationUsername, this.requestData, this.quantity = 1, @@ -272,7 +278,7 @@ class SKPaymentWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKPaymentWrapper.fromJson(Map map) { + factory SKPaymentWrapper.fromJson(Map map) { assert(map != null); return _$SKPaymentWrapperFromJson(map); } @@ -289,6 +295,7 @@ class SKPaymentWrapper { } /// The id for the product that the payment is for. + @JsonKey(defaultValue: '') final String productIdentifier; /// An opaque id for the user's account. @@ -299,7 +306,7 @@ class SKPaymentWrapper { /// account name on your server. Don’t use the Apple ID for your developer /// account, the user’s Apple ID, or the user’s plaintext account name on /// your server. - final String applicationUsername; + final String? applicationUsername; /// Reserved for future use. /// @@ -310,18 +317,26 @@ class SKPaymentWrapper { // We also provide this property to match the iOS platform. Converted to // String from NSData from ios platform using UTF8Encoding. The / default is // null. - final String requestData; + final String? requestData; /// The amount of the product this payment is for. /// /// The default is 1. The minimum is 1. The maximum is 10. + /// + /// If the object is invalid, the value could be 0. + @JsonKey(defaultValue: 0) final int quantity; - /// Produces an "ask to buy" flow in the sandbox if set to true. Default is - /// false. + /// Produces an "ask to buy" flow in the sandbox. + /// + /// Setting it to `true` will cause a transaction to be in the state [SKPaymentTransactionStateWrapper.deferred], + /// which produce an "ask to buy" prompt that interrupts the the payment flow. + /// + /// Default is `false`. /// /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox /// testing. + @JsonKey(defaultValue: false) final bool simulatesAskToBuyInSandbox; @override @@ -332,7 +347,7 @@ class SKPaymentWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPaymentWrapper typedOther = other; + final SKPaymentWrapper typedOther = other as SKPaymentWrapper; return typedOther.productIdentifier == productIdentifier && typedOther.applicationUsername == applicationUsername && typedOther.quantity == quantity && diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart index 48a18e61d4d9..2b886597adc5 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -8,11 +8,12 @@ part of 'sk_payment_queue_wrapper.dart'; SKError _$SKErrorFromJson(Map json) { return SKError( - code: json['code'] as int, - domain: json['domain'] as String, - userInfo: (json['userInfo'] as Map)?.map( - (k, e) => MapEntry(k as String, e), - ), + code: json['code'] as int? ?? 0, + domain: json['domain'] as String? ?? '', + userInfo: (json['userInfo'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {}, ); } @@ -24,11 +25,12 @@ Map _$SKErrorToJson(SKError instance) => { SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) { return SKPaymentWrapper( - productIdentifier: json['productIdentifier'] as String, - applicationUsername: json['applicationUsername'] as String, - requestData: json['requestData'] as String, - quantity: json['quantity'] as int, - simulatesAskToBuyInSandbox: json['simulatesAskToBuyInSandbox'] as bool, + productIdentifier: json['productIdentifier'] as String? ?? '', + applicationUsername: json['applicationUsername'] as String?, + requestData: json['requestData'] as String?, + quantity: json['quantity'] as int? ?? 0, + simulatesAskToBuyInSandbox: + json['simulatesAskToBuyInSandbox'] as bool? ?? false, ); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 65f6ff8871f8..9921380e6e96 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:ui' show hashValues; -import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'sk_product_wrapper.dart'; import 'sk_payment_queue_wrapper.dart'; @@ -20,13 +19,15 @@ part 'sk_payment_transaction_wrappers.g.dart'; /// This class is a Dart wrapper around [SKTransactionObserver](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver?language=objc). abstract class SKTransactionObserverWrapper { /// Triggered when any transactions are updated. - void updatedTransactions({List transactions}); + void updatedTransactions( + {required List transactions}); /// Triggered when any transactions are removed from the payment queue. - void removedTransactions({List transactions}); + void removedTransactions( + {required List transactions}); /// Triggered when there is an error while restoring transactions. - void restoreCompletedTransactionsFailed({SKError error}); + void restoreCompletedTransactionsFailed({required SKError error}); /// Triggered when payment queue has finished sending restored transactions. void paymentQueueRestoreCompletedTransactionsFinished(); @@ -41,7 +42,7 @@ abstract class SKTransactionObserverWrapper { /// continue the transaction later by calling [addPayment] with the /// `payment` param from this method. bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}); + {required SKPaymentWrapper payment, required SKProductWrapper product}); } /// The state of a transaction. @@ -85,6 +86,10 @@ enum SKPaymentTransactionStateWrapper { /// transaction to update to another state. @JsonValue(4) deferred, + + /// Indicates the transaction is in an unspecified state. + @JsonValue(-1) + unspecified, } /// Created when a payment is added to the [SKPaymentQueueWrapper]. @@ -96,16 +101,16 @@ enum SKPaymentTransactionStateWrapper { /// /// Dart wrapper around StoreKit's /// [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction?language=objc). -@JsonSerializable(nullable: true) +@JsonSerializable() class SKPaymentTransactionWrapper { /// Creates a new [SKPaymentTransactionWrapper] with the provided information. SKPaymentTransactionWrapper({ - @required this.payment, - @required this.transactionState, - @required this.originalTransaction, - @required this.transactionTimeStamp, - @required this.transactionIdentifier, - @required this.error, + required this.payment, + required this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error, }); /// Constructs an instance of this from a key value map of data. @@ -113,10 +118,7 @@ class SKPaymentTransactionWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKPaymentTransactionWrapper.fromJson(Map map) { - if (map == null) { - return null; - } + factory SKPaymentTransactionWrapper.fromJson(Map map) { return _$SKPaymentTransactionWrapperFromJson(map); } @@ -130,18 +132,21 @@ class SKPaymentTransactionWrapper { /// The original Transaction. /// - /// Only available if the [transactionState] is - /// [SKPaymentTransactionStateWrapper.restored]. When the [transactionState] + /// Only available if the [transactionState] is [SKPaymentTransactionStateWrapper.restored]. + /// Otherwise the value is `null`. + /// + /// When the [transactionState] /// is [SKPaymentTransactionStateWrapper.restored], the current transaction /// object holds a new [transactionIdentifier]. - final SKPaymentTransactionWrapper originalTransaction; + final SKPaymentTransactionWrapper? originalTransaction; /// The timestamp of the transaction. /// /// Seconds since epoch. It is only defined when the [transactionState] is /// [SKPaymentTransactionStateWrapper.purchased] or /// [SKPaymentTransactionStateWrapper.restored]. - final double transactionTimeStamp; + /// Otherwise, the value is `null`. + final double? transactionTimeStamp; /// The unique string identifer of the transaction. /// @@ -150,13 +155,15 @@ class SKPaymentTransactionWrapper { /// [SKPaymentTransactionStateWrapper.restored]. You may wish to record this /// string as part of an audit trail for App Store purchases. The value of /// this string corresponds to the same property in the receipt. - final String transactionIdentifier; + /// + /// The value is `null` if it is an unsuccessful transaction. + final String? transactionIdentifier; /// The error object /// /// Only available if the [transactionState] is /// [SKPaymentTransactionStateWrapper.failed]. - final SKError error; + final SKError? error; @override bool operator ==(Object other) { @@ -166,7 +173,8 @@ class SKPaymentTransactionWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPaymentTransactionWrapper typedOther = other; + final SKPaymentTransactionWrapper typedOther = + other as SKPaymentTransactionWrapper; return typedOther.payment == payment && typedOther.transactionState == transactionState && typedOther.originalTransaction == originalTransaction && @@ -188,8 +196,8 @@ class SKPaymentTransactionWrapper { String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); /// The payload that is used to finish this transaction. - Map toFinishMap() => { + Map toFinishMap() => { "transactionIdentifier": this.transactionIdentifier, - "productIdentifier": this.payment?.productIdentifier, + "productIdentifier": this.payment.productIdentifier, }; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart index bc520826d9fe..4c7af21bc151 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart @@ -8,19 +8,19 @@ part of 'sk_payment_transaction_wrappers.dart'; SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) { return SKPaymentTransactionWrapper( - payment: json['payment'] == null - ? null - : SKPaymentWrapper.fromJson(json['payment'] as Map), + payment: SKPaymentWrapper.fromJson( + Map.from(json['payment'] as Map)), transactionState: const SKTransactionStatusConverter() - .fromJson(json['transactionState'] as int), + .fromJson(json['transactionState'] as int?), originalTransaction: json['originalTransaction'] == null ? null : SKPaymentTransactionWrapper.fromJson( - json['originalTransaction'] as Map), - transactionTimeStamp: (json['transactionTimeStamp'] as num)?.toDouble(), - transactionIdentifier: json['transactionIdentifier'] as String, - error: - json['error'] == null ? null : SKError.fromJson(json['error'] as Map), + Map.from(json['originalTransaction'] as Map)), + transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), + transactionIdentifier: json['transactionIdentifier'] as String?, + error: json['error'] == null + ? null + : SKError.fromJson(Map.from(json['error'] as Map)), ); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index aa76971102cf..d77ea81c2d38 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'dart:ui' show hashValues; -import 'package:flutter/foundation.dart'; import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'enum_converters.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the // below generated file. Run `flutter packages pub run build_runner watch` to @@ -20,14 +20,12 @@ part 'sk_product_wrapper.g.dart'; class SkProductResponseWrapper { /// Creates an [SkProductResponseWrapper] with the given product details. SkProductResponseWrapper( - {@required this.products, @required this.invalidProductIdentifiers}); + {required this.products, required this.invalidProductIdentifiers}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKRequestMaker.startProductRequest]. - /// The `map` parameter must not be null. factory SkProductResponseWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); return _$SkProductResponseWrapperFromJson(map); } @@ -35,6 +33,7 @@ class SkProductResponseWrapper { /// /// One product in this list matches one valid product identifier passed to the [SKRequestMaker.startProductRequest]. /// Will be empty if the [SKRequestMaker.startProductRequest] method does not pass any correct product identifier. + @JsonKey(defaultValue: []) final List products; /// Stores product identifiers in the `productIdentifiers` from [SKRequestMaker.startProductRequest] that are not recognized by the App Store. @@ -42,6 +41,7 @@ class SkProductResponseWrapper { /// The App Store will not recognize a product identifier unless certain criteria are met. A detailed list of the criteria can be /// found here https://developer.apple.com/documentation/storekit/skproductsresponse/1505985-invalidproductidentifiers?language=objc. /// Will be empty if all the product identifiers are valid. + @JsonKey(defaultValue: []) final List invalidProductIdentifiers; @override @@ -52,7 +52,8 @@ class SkProductResponseWrapper { if (other.runtimeType != runtimeType) { return false; } - final SkProductResponseWrapper typedOther = other; + final SkProductResponseWrapper typedOther = + other as SkProductResponseWrapper; return DeepCollectionEquality().equals(typedOther.products, products) && DeepCollectionEquality().equals( typedOther.invalidProductIdentifiers, invalidProductIdentifiers); @@ -91,27 +92,32 @@ enum SKSubscriptionPeriodUnit { /// /// A period is defined by a [numberOfUnits] and a [unit], e.g for a 3 months period [numberOfUnits] is 3 and [unit] is a month. /// It is used as a property in [SKProductDiscountWrapper] and [SKProductWrapper]. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductSubscriptionPeriodWrapper { /// Creates an [SKProductSubscriptionPeriodWrapper] for a `numberOfUnits`x`unit` period. SKProductSubscriptionPeriodWrapper( - {@required this.numberOfUnits, @required this.unit}); + {required this.numberOfUnits, required this.unit}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductDiscountWrapper.fromJson] or [SKProductWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductSubscriptionPeriodWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductSubscriptionPeriodWrapper.fromJson( + Map? map) { + if (map == null) { + return SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day); + } return _$SKProductSubscriptionPeriodWrapperFromJson(map); } /// The number of [unit] units in this period. /// - /// Must be greater than 0. + /// Must be greater than 0 if the object is valid. + @JsonKey(defaultValue: 0) final int numberOfUnits; /// The time unit used to specify the length of this period. + @SKSubscriptionPeriodUnitConverter() final SKSubscriptionPeriodUnit unit; @override @@ -122,7 +128,8 @@ class SKProductSubscriptionPeriodWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductSubscriptionPeriodWrapper typedOther = other; + final SKProductSubscriptionPeriodWrapper typedOther = + other as SKProductSubscriptionPeriodWrapper; return typedOther.numberOfUnits == numberOfUnits && typedOther.unit == unit; } @@ -147,31 +154,34 @@ enum SKProductDiscountPaymentMode { /// User pays nothing during the discounted period. @JsonValue(2) freeTrail, + + /// Unspecified mode. + @JsonValue(-1) + unspecified, } /// Dart wrapper around StoreKit's [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc). /// /// It is used as a property in [SKProductWrapper]. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductDiscountWrapper { /// Creates an [SKProductDiscountWrapper] with the given discount details. SKProductDiscountWrapper( - {@required this.price, - @required this.priceLocale, - @required this.numberOfPeriods, - @required this.paymentMode, - @required this.subscriptionPeriod}); + {required this.price, + required this.priceLocale, + required this.numberOfPeriods, + required this.paymentMode, + required this.subscriptionPeriod}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductDiscountWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductDiscountWrapper.fromJson(Map map) { return _$SKProductDiscountWrapperFromJson(map); } /// The discounted price, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') final String price; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. @@ -179,10 +189,12 @@ class SKProductDiscountWrapper { /// The object represent the discount period length. /// - /// The value must be >= 0. + /// The value must be >= 0 if the object is valid. + @JsonKey(defaultValue: 0) final int numberOfPeriods; /// The object indicates how the discount price is charged. + @SKProductDiscountPaymentModeConverter() final SKProductDiscountPaymentMode paymentMode; /// The object represents the duration of single subscription period for the discount. @@ -199,7 +211,8 @@ class SKProductDiscountWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductDiscountWrapper typedOther = other; + final SKProductDiscountWrapper typedOther = + other as SKProductDiscountWrapper; return typedOther.price == price && typedOther.priceLocale == priceLocale && typedOther.numberOfPeriods == numberOfPeriods && @@ -216,40 +229,41 @@ class SKProductDiscountWrapper { /// /// A list of [SKProductWrapper] is returned in the [SKRequestMaker.startProductRequest] method, and /// should be stored for use when making a payment. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductWrapper { /// Creates an [SKProductWrapper] with the given product details. SKProductWrapper({ - @required this.productIdentifier, - @required this.localizedTitle, - @required this.localizedDescription, - @required this.priceLocale, - @required this.subscriptionGroupIdentifier, - @required this.price, - @required this.subscriptionPeriod, - @required this.introductoryPrice, + required this.productIdentifier, + required this.localizedTitle, + required this.localizedDescription, + required this.priceLocale, + this.subscriptionGroupIdentifier, + required this.price, + this.subscriptionPeriod, + this.introductoryPrice, }); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SkProductResponseWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductWrapper.fromJson(Map map) { return _$SKProductWrapperFromJson(map); } /// The unique identifier of the product. + @JsonKey(defaultValue: '') final String productIdentifier; /// The localizedTitle of the product. /// /// It is localized based on the current locale. + @JsonKey(defaultValue: '') final String localizedTitle; /// The localized description of the product. /// /// It is localized based on the current locale. + @JsonKey(defaultValue: '') final String localizedDescription; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. @@ -257,26 +271,29 @@ class SKProductWrapper { /// The subscription group identifier. /// + /// If the product is not a subscription, the value is `null`. + /// /// A subscription group is a collection of subscription products. /// Check [SubscriptionGroup](https://developer.apple.com/app-store/subscriptions/) for more details about subscription group. - final String subscriptionGroupIdentifier; + final String? subscriptionGroupIdentifier; /// The price of the product, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') final String price; /// The object represents the subscription period of the product. /// /// Can be [null] is the product is not a subscription. - final SKProductSubscriptionPeriodWrapper subscriptionPeriod; + final SKProductSubscriptionPeriodWrapper? subscriptionPeriod; /// The object represents the duration of single subscription period. /// - /// This is only available if you set up the introductory price in the App Store Connect, otherwise it will be null. + /// This is only available if you set up the introductory price in the App Store Connect, otherwise the value is `null`. /// Programmer is also responsible to determine if the user is eligible to receive it. See https://developer.apple.com/documentation/storekit/in-app_purchase/offering_introductory_pricing_in_your_app?language=objc /// for more details. /// The [subscriptionPeriod] of the discount is independent of the product's [subscriptionPeriod], /// and their units and duration do not have to be matched. - final SKProductDiscountWrapper introductoryPrice; + final SKProductDiscountWrapper? introductoryPrice; @override bool operator ==(Object other) { @@ -286,7 +303,7 @@ class SKProductWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductWrapper typedOther = other; + final SKProductWrapper typedOther = other as SKProductWrapper; return typedOther.productIdentifier == productIdentifier && typedOther.localizedTitle == localizedTitle && typedOther.localizedDescription == localizedDescription && @@ -319,21 +336,24 @@ class SKProductWrapper { class SKPriceLocaleWrapper { /// Creates a new price locale for `currencySymbol` and `currencyCode`. SKPriceLocaleWrapper( - {@required this.currencySymbol, @required this.currencyCode}); + {required this.currencySymbol, required this.currencyCode}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson] and [SKProductDiscountWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKPriceLocaleWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKPriceLocaleWrapper.fromJson(Map? map) { + if (map == null) { + return SKPriceLocaleWrapper(currencyCode: '', currencySymbol: ''); + } return _$SKPriceLocaleWrapperFromJson(map); } ///The currency symbol for the locale, e.g. $ for US locale. + @JsonKey(defaultValue: '') final String currencySymbol; ///The currency code for the locale, e.g. USD for US locale. + @JsonKey(defaultValue: '') final String currencyCode; @override @@ -344,7 +364,7 @@ class SKPriceLocaleWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPriceLocaleWrapper typedOther = other; + final SKPriceLocaleWrapper typedOther = other as SKPriceLocaleWrapper; return typedOther.currencySymbol == currencySymbol && typedOther.currencyCode == currencyCode; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index cf27852263ba..8c2eed3d6070 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -8,12 +8,16 @@ part of 'sk_product_wrapper.dart'; SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) { return SkProductResponseWrapper( - products: (json['products'] as List) - .map((e) => SKProductWrapper.fromJson(e as Map)) - .toList(), - invalidProductIdentifiers: (json['invalidProductIdentifiers'] as List) - .map((e) => e as String) - .toList(), + products: (json['products'] as List?) + ?.map((e) => + SKProductWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], + invalidProductIdentifiers: + (json['invalidProductIdentifiers'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], ); } @@ -27,8 +31,9 @@ Map _$SkProductResponseWrapperToJson( SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( Map json) { return SKProductSubscriptionPeriodWrapper( - numberOfUnits: json['numberOfUnits'] as int, - unit: _$enumDecodeNullable(_$SKSubscriptionPeriodUnitEnumMap, json['unit']), + numberOfUnits: json['numberOfUnits'] as int? ?? 0, + unit: const SKSubscriptionPeriodUnitConverter() + .fromJson(json['unit'] as int?), ); } @@ -36,61 +41,23 @@ Map _$SKProductSubscriptionPeriodWrapperToJson( SKProductSubscriptionPeriodWrapper instance) => { 'numberOfUnits': instance.numberOfUnits, - 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], + 'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit), }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); - } - - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; -} - -T _$enumDecodeNullable( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - -const _$SKSubscriptionPeriodUnitEnumMap = { - SKSubscriptionPeriodUnit.day: 0, - SKSubscriptionPeriodUnit.week: 1, - SKSubscriptionPeriodUnit.month: 2, - SKSubscriptionPeriodUnit.year: 3, -}; - SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { return SKProductDiscountWrapper( - price: json['price'] as String, - priceLocale: json['priceLocale'] == null - ? null - : SKPriceLocaleWrapper.fromJson(json['priceLocale'] as Map), - numberOfPeriods: json['numberOfPeriods'] as int, - paymentMode: _$enumDecodeNullable( - _$SKProductDiscountPaymentModeEnumMap, json['paymentMode']), - subscriptionPeriod: json['subscriptionPeriod'] == null - ? null - : SKProductSubscriptionPeriodWrapper.fromJson( - json['subscriptionPeriod'] as Map), + price: json['price'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + numberOfPeriods: json['numberOfPeriods'] as int? ?? 0, + paymentMode: const SKProductDiscountPaymentModeConverter() + .fromJson(json['paymentMode'] as int?), + subscriptionPeriod: SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), ); } @@ -100,34 +67,32 @@ Map _$SKProductDiscountWrapperToJson( 'price': instance.price, 'priceLocale': instance.priceLocale, 'numberOfPeriods': instance.numberOfPeriods, - 'paymentMode': - _$SKProductDiscountPaymentModeEnumMap[instance.paymentMode], + 'paymentMode': const SKProductDiscountPaymentModeConverter() + .toJson(instance.paymentMode), 'subscriptionPeriod': instance.subscriptionPeriod, }; -const _$SKProductDiscountPaymentModeEnumMap = { - SKProductDiscountPaymentMode.payAsYouGo: 0, - SKProductDiscountPaymentMode.payUpFront: 1, - SKProductDiscountPaymentMode.freeTrail: 2, -}; - SKProductWrapper _$SKProductWrapperFromJson(Map json) { return SKProductWrapper( - productIdentifier: json['productIdentifier'] as String, - localizedTitle: json['localizedTitle'] as String, - localizedDescription: json['localizedDescription'] as String, - priceLocale: json['priceLocale'] == null - ? null - : SKPriceLocaleWrapper.fromJson(json['priceLocale'] as Map), - subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String, - price: json['price'] as String, + productIdentifier: json['productIdentifier'] as String? ?? '', + localizedTitle: json['localizedTitle'] as String? ?? '', + localizedDescription: json['localizedDescription'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String?, + price: json['price'] as String? ?? '', subscriptionPeriod: json['subscriptionPeriod'] == null ? null : SKProductSubscriptionPeriodWrapper.fromJson( - json['subscriptionPeriod'] as Map), + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), introductoryPrice: json['introductoryPrice'] == null ? null - : SKProductDiscountWrapper.fromJson(json['introductoryPrice'] as Map), + : SKProductDiscountWrapper.fromJson( + Map.from(json['introductoryPrice'] as Map)), ); } @@ -145,8 +110,8 @@ Map _$SKProductWrapperToJson(SKProductWrapper instance) => SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) { return SKPriceLocaleWrapper( - currencySymbol: json['currencySymbol'] as String, - currencyCode: json['currencyCode'] as String, + currencySymbol: json['currencySymbol'] as String? ?? '', + currencyCode: json['currencyCode'] as String? ?? '', ); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart index 85af9dedc7c3..16bcb77a2c70 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart @@ -14,8 +14,9 @@ class SKReceiptManager { /// There are 2 ways to do so. Either validate locally or validate with App Store. /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). /// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt. - static Future retrieveReceiptData() { - return channel.invokeMethod( - '-[InAppPurchasePlugin retrieveReceiptData:result:]'); + static Future retrieveReceiptData() async { + return (await channel.invokeMethod( + '-[InAppPurchasePlugin retrieveReceiptData:result:]')) ?? + ''; } } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart index 959113cd66d8..c22df0a9dbdd 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -24,7 +24,7 @@ class SKRequestMaker { /// A [PlatformException] is thrown if the platform code making the request fails. Future startProductRequest( List productIdentifiers) async { - final Map productResponseMap = + final Map? productResponseMap = await channel.invokeMapMethod( '-[InAppPurchasePlugin startProductRequest:result:]', productIdentifiers, @@ -47,7 +47,8 @@ class SKRequestMaker { /// * isExpired: whether the receipt is expired. /// * isRevoked: whether the receipt has been revoked. /// * isVolumePurchase: whether the receipt is a Volume Purchase Plan receipt. - Future startRefreshReceiptRequest({Map receiptProperties}) { + Future startRefreshReceiptRequest( + {Map? receiptProperties}) { return channel.invokeMethod( '-[InAppPurchasePlugin refreshReceipt:result:]', receiptProperties, diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index 6a6c525132da..f847a81291be 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -1,30 +1,26 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.3.5+2 +version: 0.4.0 dependencies: - async: ^2.0.8 - collection: ^1.14.11 flutter: sdk: flutter - json_annotation: ^3.0.0 - meta: ^1.1.6 + json_annotation: ^4.0.0 + meta: ^1.3.0 + collection: ^1.15.0 dev_dependencies: - build_runner: ^1.0.0 - json_serializable: ^3.2.0 + build_runner: ^1.11.1 + json_serializable: ^4.0.0 flutter_test: sdk: flutter flutter_driver: sdk: flutter - in_app_purchase_example: - path: example/ - test: ^1.5.2 - shared_preferences: ^0.5.2 + test: ^1.16.0 integration_test: path: ../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: plugin: @@ -36,5 +32,5 @@ flutter: pluginClass: InAppPurchasePlugin environment: - sdk: ">=2.3.0 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" flutter: ">=1.12.13+hotfix.5" diff --git a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart index eee33a698237..d415007284c8 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -16,7 +16,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); - BillingClient billingClient; + late BillingClient billingClient; setUpAll(() => channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); @@ -96,6 +96,20 @@ void main() { equals( {'handle': 0, 'enablePendingPurchases': true})); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: methodName, + value: null, + ); + + expect( + await billingClient.startConnection( + onBillingServiceDisconnected: () {}), + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); test('endConnection', () async { @@ -151,6 +165,20 @@ void main() { expect(response.billingResult, equals(billingResult)); expect(response.skuDetailsList, contains(dummySkuDetails)); }); + + test('handles null method channel response', () async { + stubPlatform.addResponse(name: queryMethodName, value: null); + + final SkuDetailsResponseWrapper response = await billingClient + .querySkuDetails( + skuType: SkuType.inapp, skusList: ['invalid']); + + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage); + expect(response.billingResult, equals(billingResult)); + expect(response.skuDetailsList, isEmpty); + }); }); group('launchBillingFlow', () { @@ -197,6 +225,19 @@ void main() { expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], isNull); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: launchMethodName, + value: null, + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + expect( + await billingClient.launchBillingFlow(sku: skuDetails.sku), + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); group('queryPurchases', () { @@ -228,10 +269,6 @@ void main() { expect(response.purchasesList, equals(expectedList)); }); - test('checks for null params', () async { - expect(() => billingClient.queryPurchases(null), throwsAssertionError); - }); - test('handles empty purchases', () async { final BillingResponse expectedCode = BillingResponse.userCanceled; const String debugMessage = 'dummy message'; @@ -251,6 +288,23 @@ void main() { expect(response.responseCode, equals(expectedCode)); expect(response.purchasesList, isEmpty); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: queryPurchasesMethodName, + value: null, + ); + final PurchasesResultWrapper response = + await billingClient.queryPurchases(SkuType.inapp); + + expect( + response.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(response.responseCode, BillingResponse.error); + expect(response.purchasesList, isEmpty); + }); }); group('queryPurchaseHistory', () { @@ -282,11 +336,6 @@ void main() { expect(response.purchaseHistoryRecordList, equals(expectedList)); }); - test('checks for null params', () async { - expect( - () => billingClient.queryPurchaseHistory(null), throwsAssertionError); - }); - test('handles empty purchases', () async { final BillingResponse expectedCode = BillingResponse.userCanceled; const String debugMessage = 'dummy message'; @@ -303,6 +352,22 @@ void main() { expect(response.billingResult, equals(expectedBillingResult)); expect(response.purchaseHistoryRecordList, isEmpty); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: queryPurchaseHistoryMethodName, + value: null, + ); + final PurchasesHistoryResult response = + await billingClient.queryPurchaseHistory(SkuType.inapp); + + expect( + response.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(response.purchaseHistoryRecordList, isEmpty); + }); }); group('consume purchases', () { @@ -322,6 +387,21 @@ void main() { expect(billingResult, equals(expectedBillingResult)); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: consumeMethodName, + value: null, + ); + final BillingResultWrapper billingResult = await billingClient + .consumeAsync('dummy token', developerPayload: 'dummy payload'); + + expect( + billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); group('acknowledge purchases', () { @@ -342,5 +422,20 @@ void main() { expect(billingResult, equals(expectedBillingResult)); }); + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: acknowledgeMethodName, + value: null, + ); + final BillingResultWrapper billingResult = + await billingClient.acknowledgePurchase('dummy token', + developerPayload: 'dummy payload'); + + expect( + billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); } diff --git a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart index 978252a3d118..7f3de2742603 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -62,6 +62,7 @@ void main() { expect(details.purchaseID, dummyPurchase.orderId); expect(details.productID, dummyPurchase.sku); expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); + expect(details.verificationData, isNotNull); expect(details.verificationData.source, IAPSource.GooglePlay); expect(details.verificationData.localVerificationData, dummyPurchase.originalJson); @@ -111,6 +112,18 @@ void main() { expect(parsed.responseCode, equals(expected.responseCode)); expect(parsed.purchasesList, containsAll(expected.purchasesList)); }); + + test('parsed from empty map', () { + final PurchasesResultWrapper parsed = + PurchasesResultWrapper.fromJson({}); + expect( + parsed.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(parsed.responseCode, BillingResponse.error); + expect(parsed.purchasesList, isEmpty); + }); }); group('PurchasesHistoryResult', () { @@ -139,6 +152,17 @@ void main() { expect(parsed.purchaseHistoryRecordList, containsAll(expected.purchaseHistoryRecordList)); }); + + test('parsed from empty map', () { + final PurchasesHistoryResult parsed = + PurchasesHistoryResult.fromJson({}); + expect( + parsed.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(parsed.purchaseHistoryRecordList, isEmpty); + }); }); } diff --git a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart index c305e6df88cc..13715eeb9fc0 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -99,6 +99,33 @@ void main() { expect(parsed.billingResult, equals(expected.billingResult)); expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); }); + + test('fromJson creates an object with default values', () { + final SkuDetailsResponseWrapper skuDetails = + SkuDetailsResponseWrapper.fromJson({}); + expect( + skuDetails.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(skuDetails.skuDetailsList, isEmpty); + }); + }); + + group('BillingResultWrapper', () { + test('fromJson on empty map creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson({}); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); + + test('fromJson on null creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson(null); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); }); } diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart index b22737ca041b..bfcab085e26a 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart @@ -15,6 +15,7 @@ import 'package:in_app_purchase/src/in_app_purchase/app_store_connection.dart'; import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; import 'package:in_app_purchase/store_kit_wrappers.dart'; +import '../billing_client_wrappers/purchase_wrapper_test.dart'; import '../store_kit_wrappers/sk_test_stub_objects.dart'; void main() { @@ -61,10 +62,11 @@ void main() { .queryProductDetails(['123', '456', '789'].toSet()); expect(response.productDetails, []); expect(response.notFoundIDs, ['123', '456', '789']); - expect(response.error.source, IAPSource.AppStore); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); + expect(response.error, isNotNull); + expect(response.error!.source, IAPSource.AppStore); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); }); }); @@ -81,6 +83,8 @@ void main() { fakeIOSPlatform.transactions.first.transactionIdentifier); expect(response.pastPurchases.last.purchaseID, fakeIOSPlatform.transactions.last.transactionIdentifier); + expect(response.pastPurchases, isNotEmpty); + expect(response.pastPurchases.first.verificationData, isNotNull); expect( response.pastPurchases.first.verificationData.localVerificationData, 'dummy base64data'); @@ -97,7 +101,7 @@ void main() { Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { completer.complete(purchaseDetailsList); @@ -130,9 +134,10 @@ void main() { QueryPurchaseDetailsResponse response = await AppStoreConnection.instance.queryPastPurchases(); expect(response.pastPurchases, isEmpty); - expect(response.error.source, IAPSource.AppStore); - expect(response.error.message, 'error_test'); - expect(response.error.details, {'message': 'errorMessage'}); + expect(response.error, isNotNull); + expect(response.error!.source, IAPSource.AppStore); + expect(response.error!.message, 'error_test'); + expect(response.error!.details, {'message': 'errorMessage'}); }); test('receipt error should populate null to verificationData.data', @@ -142,18 +147,19 @@ void main() { await AppStoreConnection.instance.queryPastPurchases(); expect( response.pastPurchases.first.verificationData.localVerificationData, - null); + isEmpty); expect( response.pastPurchases.first.verificationData.serverVerificationData, - null); + isEmpty); }); }); group('refresh receipt data', () { test('should refresh receipt data', () async { - PurchaseVerificationData receiptData = + PurchaseVerificationData? receiptData = await AppStoreConnection.instance.refreshPurchaseVerificationData(); - expect(receiptData.source, IAPSource.AppStore); + expect(receiptData, isNotNull); + expect(receiptData!.source, IAPSource.AppStore); expect(receiptData.localVerificationData, 'refreshed receipt data'); expect(receiptData.serverVerificationData, 'refreshed receipt data'); }); @@ -168,7 +174,7 @@ void main() { Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { @@ -195,7 +201,7 @@ void main() { Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { @@ -228,16 +234,16 @@ void main() { fakeIOSPlatform.testTransactionFail = true; List details = []; Completer completer = Completer(); - IAPError error; + late IAPError error; Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); purchaseDetailsList.forEach((purchaseDetails) { if (purchaseDetails.status == PurchaseStatus.error) { - error = purchaseDetails.error; + error = purchaseDetails.error!; completer.complete(error); subscription.cancel(); } @@ -263,7 +269,7 @@ void main() { Completer completer = Completer(); Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); purchaseDetailsList.forEach((purchaseDetails) { @@ -288,7 +294,9 @@ void main() { group('consume purchase', () { test('should throw when calling consume purchase on iOS', () async { - expect(() => AppStoreConnection.instance.consumePurchase(null), + expect( + () => AppStoreConnection.instance + .consumePurchase(PurchaseDetails.fromPurchase(dummyPurchase)), throwsUnsupportedError); }); }); @@ -300,16 +308,16 @@ class FakeIOSPlatform { } // pre-configured store informations - String receiptData; - Set validProductIDs; - Map validProducts; - List transactions; - List finishedTransactions; - bool testRestoredTransactionsNull; - bool testTransactionFail; - PlatformException queryProductException; - PlatformException restoreException; - SKError testRestoredError; + String? receiptData; + late Set validProductIDs; + late Map validProducts; + late List transactions; + late List finishedTransactions; + late bool testRestoredTransactionsNull; + late bool testTransactionFail; + PlatformException? queryProductException; + PlatformException? restoreException; + SKError? testRestoredError; void reset() { transactions = []; @@ -317,7 +325,8 @@ class FakeIOSPlatform { validProductIDs = ['123', '456'].toSet(); validProducts = Map(); for (String validID in validProductIDs) { - Map productWrapperMap = buildProductMap(dummyProductWrapper); + Map productWrapperMap = + buildProductMap(dummyProductWrapper); productWrapperMap['productIdentifier'] = validID; validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); } @@ -350,7 +359,7 @@ class FakeIOSPlatform { SKPaymentTransactionWrapper createPendingTransaction(String id) { return SKPaymentTransactionWrapper( - transactionIdentifier: null, + transactionIdentifier: '', payment: SKPaymentWrapper(productIdentifier: id), transactionState: SKPaymentTransactionStateWrapper.purchasing, transactionTimeStamp: 123123.121, @@ -371,7 +380,7 @@ class FakeIOSPlatform { SKPaymentTransactionWrapper createFailedTransaction(String productId) { return SKPaymentTransactionWrapper( - transactionIdentifier: null, + transactionIdentifier: '', payment: SKPaymentWrapper(productIdentifier: productId), transactionState: SKPaymentTransactionStateWrapper.failed, transactionTimeStamp: 123123.121, @@ -388,7 +397,7 @@ class FakeIOSPlatform { return Future.value(true); case '-[InAppPurchasePlugin startProductRequest:result:]': if (queryProductException != null) { - throw queryProductException; + throw queryProductException!; } List productIDS = List.castFrom(call.arguments); @@ -399,7 +408,7 @@ class FakeIOSPlatform { if (!validProductIDs.contains(productID)) { invalidFound.add(productID); } else { - products.add(validProducts[productID]); + products.add(validProducts[productID]!); } } SkProductResponseWrapper response = SkProductResponseWrapper( @@ -408,11 +417,11 @@ class FakeIOSPlatform { buildProductResponseMap(response)); case '-[InAppPurchasePlugin restoreTransactions:result:]': if (restoreException != null) { - throw restoreException; + throw restoreException!; } if (testRestoredError != null) { AppStoreConnection.observer - .restoreCompletedTransactionsFailed(error: testRestoredError); + .restoreCompletedTransactionsFailed(error: testRestoredError!); return Future.sync(() {}); } if (!testRestoredTransactionsNull) { @@ -428,7 +437,6 @@ class FakeIOSPlatform { } else { throw PlatformException(code: 'no_receipt_data'); } - break; case '-[InAppPurchasePlugin refreshReceipt:result:]': receiptData = 'refreshed receipt data'; return Future.sync(() {}); @@ -445,7 +453,8 @@ class FakeIOSPlatform { .updatedTransactions(transactions: [transaction_failed]); } else { SKPaymentTransactionWrapper transaction_finished = - createPurchasedTransaction(id, transaction.transactionIdentifier); + createPurchasedTransaction( + id, transaction.transactionIdentifier ?? ''); AppStoreConnection.observer .updatedTransactions(transactions: [transaction_finished]); } diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart index 9294d2b60d1e..79c2ee436c5c 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart @@ -24,7 +24,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); - GooglePlayConnection connection; + late GooglePlayConnection connection; const String startConnectionCall = 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; @@ -149,10 +149,11 @@ void main() { await connection.queryProductDetails(['invalid'].toSet()); expect(response.notFoundIDs, ['invalid']); expect(response.productDetails, isEmpty); - expect(response.error.source, IAPSource.GooglePlay); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); + expect(response.error, isNotNull); + expect(response.error!.source, IAPSource.GooglePlay); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); }); }); @@ -172,8 +173,10 @@ void main() { final QueryPurchaseDetailsResponse response = await connection.queryPastPurchases(); expect(response.pastPurchases, isEmpty); - expect(response.error.message, BillingResponse.developerError.toString()); - expect(response.error.source, IAPSource.GooglePlay); + expect(response.error, isNotNull); + expect( + response.error!.message, BillingResponse.developerError.toString()); + expect(response.error!.source, IAPSource.GooglePlay); }); test('returns SkuDetailsResponseWrapper', () async { @@ -221,9 +224,10 @@ void main() { final QueryPurchaseDetailsResponse response = await connection.queryPastPurchases(); expect(response.pastPurchases, isEmpty); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); + expect(response.error, isNotNull); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); }); }); @@ -277,7 +281,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -320,7 +324,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -334,9 +338,9 @@ void main() { PurchaseDetails result = await completer.future; expect(result.error, isNotNull); - expect(result.error.source, IAPSource.GooglePlay); + expect(result.error!.source, IAPSource.GooglePlay); expect(result.status, PurchaseStatus.error); - expect(result.purchaseID, isNull); + expect(result.purchaseID, isEmpty); }); test('buy consumable with auto consume, serializes and deserializes data', @@ -392,7 +396,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -407,7 +411,8 @@ void main() { // Verify that the result has succeeded PurchaseDetails result = await completer.future; expect(launchResult, isTrue); - expect(result.billingClientPurchase.purchaseToken, + expect(result.billingClientPurchase, isNotNull); + expect(result.billingClientPurchase!.purchaseToken, await consumeCompleter.future); expect(result.status, PurchaseStatus.purchased); expect(result.error, isNull); @@ -501,7 +506,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -515,11 +520,12 @@ void main() { // Verify that the result has an error for the failed consumption PurchaseDetails result = await completer.future; - expect(result.billingClientPurchase.purchaseToken, + expect(result.billingClientPurchase, isNotNull); + expect(result.billingClientPurchase!.purchaseToken, await consumeCompleter.future); expect(result.status, PurchaseStatus.error); expect(result.error, isNotNull); - expect(result.error.code, kConsumptionFailedErrorCode); + expect(result.error!.code, kConsumptionFailedErrorCode); }); test( @@ -574,7 +580,7 @@ void main() { Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { consumeCompleter.complete(null); subscription.cancel(); @@ -629,10 +635,6 @@ void main() { await GooglePlayConnection.instance.completePurchase( purchaseDetails, developerPayload: 'dummy payload'); - print('pending ${billingResultWrapper.responseCode}'); - print('expectedBillingResult ${expectedBillingResult.responseCode}'); - print('pending ${billingResultWrapper.debugMessage}'); - print('expectedBillingResult ${expectedBillingResult.debugMessage}'); expect(billingResultWrapper, equals(expectedBillingResult)); completer.complete(billingResultWrapper); } diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index 92ffbc5797e3..d41a1269d6c9 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -20,6 +20,10 @@ void main() { setUp(() {}); + tearDown(() { + fakeIOSPlatform.testReturnNull = false; + }); + group('sk_request_maker', () { test('get products method channel', () async { SkProductResponseWrapper productResponseWrapper = @@ -55,7 +59,7 @@ void main() { test('get products method channel should throw exception', () async { fakeIOSPlatform.getProductRequestFailTest = true; expect( - SKRequestMaker().startProductRequest(['xxx']), + SKRequestMaker().startProductRequest(['xxx']), throwsException, ); fakeIOSPlatform.getProductRequestFailTest = false; @@ -63,10 +67,11 @@ void main() { test('refreshed receipt', () async { int receiptCountBefore = fakeIOSPlatform.refreshReceipt; - await SKRequestMaker() - .startRefreshReceiptRequest(receiptProperties: {"isExpired": true}); + await SKRequestMaker().startRefreshReceiptRequest( + receiptProperties: {"isExpired": true}); expect(fakeIOSPlatform.refreshReceipt, receiptCountBefore + 1); - expect(fakeIOSPlatform.refreshReceiptParam, {"isExpired": true}); + expect(fakeIOSPlatform.refreshReceiptParam, + {"isExpired": true}); }); }); @@ -83,6 +88,12 @@ void main() { expect(await SKPaymentQueueWrapper.canMakePayments(), true); }); + test('canMakePayment returns false if method channel returns null', + () async { + fakeIOSPlatform.testReturnNull = true; + expect(await SKPaymentQueueWrapper.canMakePayments(), false); + }); + test('transactions should return a valid list of transactions', () async { expect(await SKPaymentQueueWrapper().transactions(), isNotEmpty); }); @@ -127,20 +138,20 @@ void main() { class FakeIOSPlatform { FakeIOSPlatform() { channel.setMockMethodCallHandler(onMethodCall); - getProductRequestFailTest = false; } // get product request - List startProductRequestParam; - bool getProductRequestFailTest; + List startProductRequestParam = []; + bool getProductRequestFailTest = false; + bool testReturnNull = false; // refresh receipt request int refreshReceipt = 0; - Map refreshReceiptParam; + late Map refreshReceiptParam; // payment queue List payments = []; List> transactionsFinished = []; - String applicationNameHasTransactionRestored; + String applicationNameHasTransactionRestored = ''; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -157,18 +168,24 @@ class FakeIOSPlatform { buildProductResponseMap(dummyProductResponseWrapper)); case '-[InAppPurchasePlugin refreshReceipt:result:]': refreshReceipt++; - refreshReceiptParam = call.arguments; + refreshReceiptParam = + Map.castFrom(call.arguments); return Future.sync(() {}); // receipt manager case '-[InAppPurchasePlugin retrieveReceiptData:result:]': return Future.value('receipt data'); // payment queue case '-[SKPaymentQueue canMakePayments:]': + if (testReturnNull) { + return Future.value(null); + } return Future.value(true); case '-[SKPaymentQueue transactions]': - return Future>.value([buildTransactionMap(dummyTransaction)]); + return Future>.value( + [buildTransactionMap(dummyTransaction)]); case '-[InAppPurchasePlugin addPayment:result:]': - payments.add(SKPaymentWrapper.fromJson(call.arguments)); + payments.add(SKPaymentWrapper.fromJson( + Map.from(call.arguments))); return Future.sync(() {}); case '-[InAppPurchasePlugin finishTransaction:result:]': transactionsFinished.add(Map.from(call.arguments)); @@ -182,16 +199,18 @@ class FakeIOSPlatform { } class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { - void updatedTransactions({List transactions}) {} + void updatedTransactions( + {required List transactions}) {} - void removedTransactions({List transactions}) {} + void removedTransactions( + {required List transactions}) {} - void restoreCompletedTransactionsFailed({SKError error}) {} + void restoreCompletedTransactionsFailed({required SKError error}) {} void paymentQueueRestoreCompletedTransactionsFinished() {} bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}) { + {required SKPaymentWrapper payment, required SKProductWrapper product}) { return true; } } diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart index 2a9066f05c53..6e1f59bf377e 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart @@ -3,11 +3,11 @@ // found in the LICENSE file. import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; +import 'package:in_app_purchase/store_kit_wrappers.dart'; import 'package:test/test.dart'; import 'package:in_app_purchase/src/store_kit_wrappers/sk_product_wrapper.dart'; import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; -import 'package:in_app_purchase/store_kit_wrappers.dart'; import 'sk_test_stub_objects.dart'; void main() { @@ -17,17 +17,17 @@ void main() { () { final SKProductSubscriptionPeriodWrapper wrapper = SKProductSubscriptionPeriodWrapper.fromJson( - buildSubscriptionPeriodMap(dummySubscription)); + buildSubscriptionPeriodMap(dummySubscription)!); expect(wrapper, equals(dummySubscription)); }); test( - 'SKProductSubscriptionPeriodWrapper should have properties to be null if map is empty', + 'SKProductSubscriptionPeriodWrapper should have properties to be default values if map is empty', () { final SKProductSubscriptionPeriodWrapper wrapper = SKProductSubscriptionPeriodWrapper.fromJson({}); - expect(wrapper.numberOfUnits, null); - expect(wrapper.unit, null); + expect(wrapper.numberOfUnits, 0); + expect(wrapper.unit, SKSubscriptionPeriodUnit.day); }); test( @@ -39,15 +39,19 @@ void main() { }); test( - 'SKProductDiscountWrapper should have properties to be null if map is empty', + 'SKProductDiscountWrapper should have properties to be default if map is empty', () { final SKProductDiscountWrapper wrapper = SKProductDiscountWrapper.fromJson({}); - expect(wrapper.price, null); - expect(wrapper.priceLocale, null); - expect(wrapper.numberOfPeriods, null); - expect(wrapper.paymentMode, null); - expect(wrapper.subscriptionPeriod, null); + expect(wrapper.price, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); + expect(wrapper.numberOfPeriods, 0); + expect(wrapper.paymentMode, SKProductDiscountPaymentMode.payAsYouGo); + expect( + wrapper.subscriptionPeriod, + SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day)); }); test('SKProductWrapper should have property values consistent with map', @@ -57,16 +61,18 @@ void main() { expect(wrapper, equals(dummyProductWrapper)); }); - test('SKProductWrapper should have properties to be null if map is empty', + test( + 'SKProductWrapper should have properties to be default if map is empty', () { final SKProductWrapper wrapper = SKProductWrapper.fromJson({}); - expect(wrapper.productIdentifier, null); - expect(wrapper.localizedTitle, null); - expect(wrapper.localizedDescription, null); - expect(wrapper.priceLocale, null); + expect(wrapper.productIdentifier, ''); + expect(wrapper.localizedTitle, ''); + expect(wrapper.localizedDescription, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); expect(wrapper.subscriptionGroupIdentifier, null); - expect(wrapper.price, null); + expect(wrapper.price, ''); expect(wrapper.subscriptionPeriod, null); }); @@ -132,7 +138,8 @@ void main() { PurchaseDetails.fromSKTransaction(dummyTransaction, 'receipt data'); expect(dummyTransaction.transactionIdentifier, details.purchaseID); expect(dummyTransaction.payment.productIdentifier, details.productID); - expect((dummyTransaction.transactionTimeStamp * 1000).toInt().toString(), + expect(dummyTransaction.transactionTimeStamp, isNotNull); + expect((dummyTransaction.transactionTimeStamp! * 1000).toInt().toString(), details.transactionDate); expect(details.verificationData.localVerificationData, 'receipt data'); expect(details.verificationData.serverVerificationData, 'receipt data'); @@ -141,6 +148,29 @@ void main() { expect(details.billingClientPurchase, null); expect(details.pendingCompletePurchase, true); }); + + test('SKPaymentTransactionWrapper.toFinishMap set correct value', () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionIdentifier: 'abcd'); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], 'abcd'); + expect(finishMap['productIdentifier'], dummyPayment.productIdentifier); + }); + + test( + 'SKPaymentTransactionWrapper.toFinishMap should set transactionIdentifier to null when necessary', + () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], null); + }); + test('Should generate correct map of the payment object', () { Map map = dummyPayment.toMap(); expect(map['productIdentifier'], dummyPayment.productIdentifier); diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart index c976e80a90a5..f7d86f5cf59b 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -74,8 +74,11 @@ Map buildLocaleMap(SKPriceLocaleWrapper local) { }; } -Map buildSubscriptionPeriodMap( - SKProductSubscriptionPeriodWrapper sub) { +Map? buildSubscriptionPeriodMap( + SKProductSubscriptionPeriodWrapper? sub) { + if (sub == null) { + return null; + } return { 'numberOfUnits': sub.numberOfUnits, 'unit': SKSubscriptionPeriodUnit.values.indexOf(sub.unit), @@ -104,7 +107,7 @@ Map buildProductMap(SKProductWrapper product) { 'price': product.price, 'subscriptionPeriod': buildSubscriptionPeriodMap(product.subscriptionPeriod), - 'introductoryPrice': buildDiscountMap(product.introductoryPrice), + 'introductoryPrice': buildDiscountMap(product.introductoryPrice!), }; } @@ -129,17 +132,16 @@ Map buildErrorMap(SKError error) { Map buildTransactionMap( SKPaymentTransactionWrapper transaction) { - if (transaction == null) { - return null; - } - Map map = { + Map map = { 'transactionState': SKPaymentTransactionStateWrapper.values .indexOf(SKPaymentTransactionStateWrapper.purchased), 'payment': transaction.payment.toMap(), - 'originalTransaction': buildTransactionMap(transaction.originalTransaction), + 'originalTransaction': transaction.originalTransaction == null + ? null + : buildTransactionMap(transaction.originalTransaction!), 'transactionTimeStamp': transaction.transactionTimeStamp, 'transactionIdentifier': transaction.transactionIdentifier, - 'error': buildErrorMap(transaction.error), + 'error': buildErrorMap(transaction.error!), }; return map; } diff --git a/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart b/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart index 312479573a68..431d8859d44d 100644 --- a/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart +++ b/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart @@ -9,19 +9,19 @@ typedef void AdditionalSteps(dynamic args); class StubInAppPurchasePlatform { Map _expectedCalls = {}; - Map _additionalSteps = {}; + Map _additionalSteps = {}; void addResponse( - {String name, + {required String name, dynamic value, - AdditionalSteps additionalStepBeforeReturn}) { + AdditionalSteps? additionalStepBeforeReturn}) { _additionalSteps[name] = additionalStepBeforeReturn; _expectedCalls[name] = value; } List _previousCalls = []; List get previousCalls => _previousCalls; - MethodCall previousCallMatching(String name) => _previousCalls - .firstWhere((MethodCall call) => call.method == name, orElse: () => null); + MethodCall previousCallMatching(String name) => + _previousCalls.firstWhere((MethodCall call) => call.method == name); int countPreviousCalls(String name) => _previousCalls.where((MethodCall call) => call.method == name).length; @@ -35,7 +35,7 @@ class StubInAppPurchasePlatform { _previousCalls.add(call); if (_expectedCalls.containsKey(call.method)) { if (_additionalSteps[call.method] != null) { - _additionalSteps[call.method](call.arguments); + _additionalSteps[call.method]!(call.arguments); } return Future.sync(() => _expectedCalls[call.method]); } else { diff --git a/script/nnbd_plugins.sh b/script/nnbd_plugins.sh index 112dccfcbba8..4feee1ed6f8b 100644 --- a/script/nnbd_plugins.sh +++ b/script/nnbd_plugins.sh @@ -32,6 +32,7 @@ readonly NNBD_PLUGINS_LIST=( "video_player" "webview_flutter" "wifi_info_flutter" + "in_app_purchase" ) # This list contains the list of plugins that have *not* been diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index af009952856e..670fedaf2fa1 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -97,7 +97,7 @@ class PublishCheckCommand extends PluginCommand { await stdInCompleter.future; final String output = outputBuffer.toString(); - return output.contains('Package has 1 warning.') && + return output.contains('Package has 1 warning') && output.contains( 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); }