From 612078dc07c862e1eda59db03205fa63e9d372d1 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 17 Feb 2021 17:42:44 -0800 Subject: [PATCH] review --- packages/in_app_purchase/CHANGELOG.md | 2 +- .../in_app_purchase/example/lib/main.dart | 17 ++-- packages/in_app_purchase/example/pubspec.yaml | 6 +- .../ios/Classes/FIAPReceiptManager.m | 7 -- .../ios/Classes/InAppPurchasePlugin.m | 2 +- .../billing_client_wrapper.dart | 14 +--- .../purchase_wrapper.dart | 17 ++-- .../purchase_wrapper.g.dart | 75 +++--------------- .../in_app_purchase/app_store_connection.dart | 4 +- .../src/in_app_purchase/purchase_details.dart | 8 +- .../store_kit_wrappers/enum_converters.dart | 50 ++++++++++++ .../store_kit_wrappers/enum_converters.g.dart | 22 +++++- .../sk_payment_queue_wrapper.dart | 23 ++++-- .../sk_payment_queue_wrapper.g.dart | 2 +- .../sk_product_wrapper.dart | 26 ++++--- .../sk_product_wrapper.g.dart | 77 ++++++------------- packages/in_app_purchase/pubspec.yaml | 9 ++- .../store_kit_wrappers/sk_product_test.dart | 35 +++++++++ script/nnbd_plugins.sh | 1 + 19 files changed, 211 insertions(+), 186 deletions(-) diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index c226d58f710d..79f64d5bda53 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.4.0-nullsafety.0 +## 0.4.0 * Migrate to nullsafety. * Deprecate `sandboxTesting`, introduce `simulatesAskToBuyInSandbox`. diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 6c08ac156ba2..82cd509b30be 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.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. -// @dart = 2.10 import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -33,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 = []; @@ -41,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); @@ -77,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 = []; @@ -147,7 +146,7 @@ class _MyAppState extends State<_MyApp> { ); } else { stack.add(Center( - child: Text(_queryProductError), + child: Text(_queryProductError!), )); } if (_purchasePending) { @@ -236,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, @@ -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 b4b202263e19..5b3b3432c8b5 100644 --- a/packages/in_app_purchase/example/pubspec.yaml +++ b/packages/in_app_purchase/example/pubspec.yaml @@ -5,7 +5,7 @@ author: Flutter Team dependencies: flutter: sdk: flutter - shared_preferences: ^0.5.2 + shared_preferences: ^2.0.0-nullsafety dev_dependencies: flutter_driver: @@ -19,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 <3.0.0" flutter: ">=1.9.1+hotfix.2" 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 46c4ab11836c..9b44ad766a98 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -290,7 +290,7 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { }]; } -#pragma mark - delegatestransactionIdentifier: +#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 84c5034873c9..eea370531e83 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,8 +53,6 @@ class BillingClient { bool _enablePendingPurchases = false; /// Creates a billing client. - /// - /// The `onPurchasesUpdated` parameter must not be null. BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { channel.setMethodCallHandler(callHandler); _callbacks[kOnPurchasesUpdated] = [onPurchasesUpdated]; @@ -74,11 +72,9 @@ class BillingClient { /// [`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 { - bool? ready = await channel.invokeMethod('BillingClient#isReady()'); - if (ready == null) { - return false; - } - return ready; + final bool? ready = + await channel.invokeMethod('BillingClient#isReady()'); + return ready ?? false; } /// Enable the [BillingClientWrapper] to handle pending purchases. @@ -234,7 +230,6 @@ 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)) @@ -266,7 +261,6 @@ 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)) @@ -292,7 +286,7 @@ class BillingClient { final PurchasesUpdatedListener listener = _callbacks[kOnPurchasesUpdated]!.first as PurchasesUpdatedListener; listener(PurchasesResultWrapper.fromJson( - Map.from(call.arguments))); + call.arguments.cast())); break; case _kOnBillingServiceDisconnected: final int handle = call.arguments['handle']; 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 26d59af2dce8..bdd738a56c5d 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 @@ -35,7 +35,7 @@ class PurchaseWrapper { required this.sku, required this.isAutoRenewing, required this.originalJson, - required this.developerPayload, + this.developerPayload, required this.isAcknowledged, required this.purchaseState}); @@ -104,7 +104,9 @@ class PurchaseWrapper { /// /// For [SkuType.subs] this means that the subscription is canceled when it is /// false. - final bool? isAutoRenewing; + /// + /// The value is `false` for [SkuType.inapp] products. + final bool isAutoRenewing; /// Details about this purchase, in JSON. /// @@ -116,8 +118,9 @@ class PurchaseWrapper { final String originalJson; /// The payload specified by the developer when the purchase was acknowledged or consumed. - @JsonKey(defaultValue: '') - 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. /// @@ -186,8 +189,9 @@ class PurchaseHistoryRecordWrapper { final String originalJson; /// The payload specified by the developer when the purchase was acknowledged or consumed. - @JsonKey(defaultValue: '') - final String developerPayload; + /// + /// If the value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + final String? developerPayload; @override bool operator ==(Object other) { @@ -248,7 +252,6 @@ class PurchasesResultWrapper { /// /// This can represent either the status of the "query purchase history" half /// of the operation and the "user made purchases" transaction itself. - @JsonKey(defaultValue: BillingResponse.error) final BillingResponse responseCode; /// The list of successful purchases made in this transaction. 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 6f4c8d86ab5d..ca7bc8b420f9 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 @@ -14,12 +14,12 @@ PurchaseWrapper _$PurchaseWrapperFromJson(Map json) { purchaseToken: json['purchaseToken'] as String? ?? '', signature: json['signature'] as String? ?? '', sku: json['sku'] as String? ?? '', - isAutoRenewing: json['isAutoRenewing'] as bool?, + isAutoRenewing: json['isAutoRenewing'] as bool, originalJson: json['originalJson'] as String? ?? '', - developerPayload: json['developerPayload'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, isAcknowledged: json['isAcknowledged'] as bool? ?? false, purchaseState: - _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']), + const PurchaseStateConverter().fromJson(json['purchaseState'] as int?), ); } @@ -35,41 +35,10 @@ Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => 'originalJson': instance.originalJson, 'developerPayload': instance.developerPayload, 'isAcknowledged': instance.isAcknowledged, - 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], + 'purchaseState': + const PurchaseStateConverter().toJson(instance.purchaseState), }; -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - 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 _$PurchaseStateWrapperEnumMap = { - PurchaseStateWrapper.unspecified_state: 0, - PurchaseStateWrapper.purchased: 1, - PurchaseStateWrapper.pending: 2, -}; - PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { return PurchaseHistoryRecordWrapper( purchaseTime: json['purchaseTime'] as int? ?? 0, @@ -77,7 +46,7 @@ PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { signature: json['signature'] as String? ?? '', sku: json['sku'] as String? ?? '', originalJson: json['originalJson'] as String? ?? '', - developerPayload: json['developerPayload'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, ); } @@ -95,8 +64,7 @@ Map _$PurchaseHistoryRecordWrapperToJson( PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { return PurchasesResultWrapper( responseCode: - _$enumDecodeNullable(_$BillingResponseEnumMap, json['responseCode']) ?? - BillingResponse.error, + const BillingResponseConverter().fromJson(json['responseCode'] as int?), billingResult: BillingResultWrapper.fromJson( Map.from(json['billingResult'] as Map)), purchasesList: (json['purchasesList'] as List?) @@ -111,36 +79,11 @@ Map _$PurchasesResultWrapperToJson( PurchasesResultWrapper instance) => { 'billingResult': instance.billingResult, - 'responseCode': _$BillingResponseEnumMap[instance.responseCode], + 'responseCode': + const BillingResponseConverter().toJson(instance.responseCode), 'purchasesList': instance.purchasesList, }; -K? _$enumDecodeNullable( - Map enumValues, - dynamic source, { - K? unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - -const _$BillingResponseEnumMap = { - BillingResponse.serviceTimeout: -3, - BillingResponse.featureNotSupported: -2, - BillingResponse.serviceDisconnected: -1, - BillingResponse.ok: 0, - BillingResponse.userCanceled: 1, - BillingResponse.serviceUnavailable: 2, - BillingResponse.billingUnavailable: 3, - BillingResponse.itemUnavailable: 4, - BillingResponse.developerError: 5, - BillingResponse.error: 6, - BillingResponse.itemAlreadyOwned: 7, - BillingResponse.itemNotOwned: 8, -}; - PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) { return PurchasesHistoryResult( billingResult: BillingResultWrapper.fromJson( 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 23c0a2872809..f1aa1e348bf8 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 @@ -60,7 +60,9 @@ class AppStoreConnection implements InAppPurchaseConnection { productIdentifier: purchaseParam.productDetails.id, quantity: 1, applicationUsername: purchaseParam.applicationUserName, - simulatesAskToBuyInSandbox: purchaseParam.simulatesAskToBuyInSandbox, + // ignore: deprecated_member_use_from_same_package + simulatesAskToBuyInSandbox: purchaseParam.simulatesAskToBuyInSandbox || + purchaseParam.sandboxTesting, requestData: null)); return true; // There's no error feedback from iOS here to return. } 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 88c4a5d6a422..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 @@ -107,11 +107,15 @@ class PurchaseParam { /// @deprecated Use [simulatesAskToBuyInSandbox] instead. /// - /// This parameter has no effect and will be removed shortly. + /// 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; } @@ -142,7 +146,7 @@ class PurchaseDetails { /// /// Milliseconds since epoch. /// - /// The value is `null` is [status] is not [PurchaseStatus.purchased]. + /// The value is `null` if [status] is not [PurchaseStatus.purchased]. final String? transactionDate; /// The status that this [PurchaseDetails] is currently on. 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 683711cc69d5..313bea13e7ea 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 @@ -48,8 +48,58 @@ class SKTransactionStatusConverter _$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 { 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 a3b7df0d31f5..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,12 +9,18 @@ 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], }; K _$enumDecode( @@ -51,3 +57,17 @@ const _$SKPaymentTransactionStateWrapperEnumMap = { 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 5be730a6605d..960ad7bf28d2 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 @@ -139,13 +139,14 @@ class SKPaymentQueueWrapper { 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(() { - return _observer!.updatedTransactions(transactions: transactions); + return observer.updatedTransactions(transactions: transactions); }); } case 'removedTransactions': @@ -153,20 +154,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 +177,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); @@ -319,11 +320,17 @@ class SKPaymentWrapper { /// The amount of the product this payment is for. /// /// The default is 1. The minimum is 1. The maximum is 10. - @JsonKey(defaultValue: 1) + /// + /// 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. 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 d5d068f2d074..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 @@ -28,7 +28,7 @@ SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) { productIdentifier: json['productIdentifier'] as String? ?? '', applicationUsername: json['applicationUsername'] as String?, requestData: json['requestData'] as String?, - quantity: json['quantity'] as int? ?? 1, + 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_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index b11e4e651847..e85ef991bcdd 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 @@ -5,6 +5,7 @@ import 'dart:ui' show hashValues; 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 @@ -24,7 +25,6 @@ class SkProductResponseWrapper { /// 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) { return _$SkProductResponseWrapperFromJson(map); } @@ -101,19 +101,22 @@ class SKProductSubscriptionPeriodWrapper { /// 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) { + 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. - @JsonKey(defaultValue: 1) + /// 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 @@ -172,7 +175,6 @@ class SKProductDiscountWrapper { /// 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) { return _$SKProductDiscountWrapperFromJson(map); } @@ -186,11 +188,12 @@ class SKProductDiscountWrapper { /// The object represent the discount period length. /// - /// The value must be >= 0. - @JsonKey(defaultValue: 1) + /// 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. @@ -242,7 +245,6 @@ class SKProductWrapper { /// 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) { return _$SKProductWrapperFromJson(map); } @@ -338,8 +340,10 @@ class SKPriceLocaleWrapper { /// 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) { + factory SKPriceLocaleWrapper.fromJson(Map? map) { + if (map == null) { + return SKPriceLocaleWrapper(currencyCode: '', currencySymbol: ''); + } return _$SKPriceLocaleWrapperFromJson(map); } 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 5a8696956cda..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 @@ -31,8 +31,9 @@ Map _$SkProductResponseWrapperToJson( SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( Map json) { return SKProductSubscriptionPeriodWrapper( - numberOfUnits: json['numberOfUnits'] as int? ?? 1, - unit: _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']), + numberOfUnits: json['numberOfUnits'] as int? ?? 0, + unit: const SKSubscriptionPeriodUnitConverter() + .fromJson(json['unit'] as int?), ); } @@ -40,52 +41,23 @@ Map _$SKProductSubscriptionPeriodWrapperToJson( SKProductSubscriptionPeriodWrapper instance) => { 'numberOfUnits': instance.numberOfUnits, - 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], + 'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit), }; -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - 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 _$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: SKPriceLocaleWrapper.fromJson( - Map.from(json['priceLocale'] as Map)), - numberOfPeriods: json['numberOfPeriods'] as int? ?? 1, - paymentMode: _$enumDecode( - _$SKProductDiscountPaymentModeEnumMap, json['paymentMode']), + 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( - Map.from(json['subscriptionPeriod'] as Map)), + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), ); } @@ -95,31 +67,28 @@ 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, - SKProductDiscountPaymentMode.unspecified: -1, -}; - SKProductWrapper _$SKProductWrapperFromJson(Map json) { return SKProductWrapper( productIdentifier: json['productIdentifier'] as String? ?? '', localizedTitle: json['localizedTitle'] as String? ?? '', localizedDescription: json['localizedDescription'] as String? ?? '', - priceLocale: SKPriceLocaleWrapper.fromJson( - Map.from(json['priceLocale'] as Map)), + 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( - Map.from(json['subscriptionPeriod'] as Map)), + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), introductoryPrice: json['introductoryPrice'] == null ? null : SKProductDiscountWrapper.fromJson( diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index a65bdd537da8..ea835e917411 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -1,17 +1,18 @@ 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.4.0-nullsafety.0 +version: 0.4.0 dependencies: flutter: sdk: flutter - json_annotation: ^4.0.0-nullsafety.0 + json_annotation: ^4.0.0 meta: ^1.3.0-nullsafety.6 + collection: ^1.15.0 dev_dependencies: build_runner: ^1.11.1 - json_serializable: ^4.0.0-nullsafety.0 + json_serializable: ^4.0.0 flutter_test: sdk: flutter flutter_driver: @@ -19,7 +20,7 @@ dev_dependencies: test: ^1.16.0 integration_test: path: ../integration_test - pedantic: ^1.10.0-nullsafety.3 + pedantic: ^1.10.0 flutter: plugin: 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 7f2a7e8afb3d..636b6724c6b3 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 @@ -21,6 +21,15 @@ void main() { expect(wrapper, equals(dummySubscription)); }); + test( + 'SKProductSubscriptionPeriodWrapper should have properties to be default values if map is empty', + () { + final SKProductSubscriptionPeriodWrapper wrapper = + SKProductSubscriptionPeriodWrapper.fromJson({}); + expect(wrapper.numberOfUnits, 0); + expect(wrapper.unit, SKSubscriptionPeriodUnit.day); + }); + test( 'SKProductDiscountWrapper should have property values consistent with map', () { @@ -29,6 +38,18 @@ void main() { expect(wrapper, equals(dummyDiscount)); }); + test( + 'SKProductDiscountWrapper should have properties to be default if map is empty', + () { + final SKProductDiscountWrapper wrapper = + SKProductDiscountWrapper.fromJson({}); + 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', () { final SKProductWrapper wrapper = @@ -36,6 +57,20 @@ void main() { expect(wrapper, equals(dummyProductWrapper)); }); + test('SKProductWrapper should have properties to be default if map is empty', + () { + final SKProductWrapper wrapper = + SKProductWrapper.fromJson({}); + expect(wrapper.productIdentifier, ''); + expect(wrapper.localizedTitle, ''); + expect(wrapper.localizedDescription, ''); + expect(wrapper.priceLocale, SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); + expect(wrapper.subscriptionGroupIdentifier, null); + expect(wrapper.price, ''); + expect(wrapper.subscriptionPeriod, null); + }); + + test('toProductDetails() should return correct Product object', () { final SKProductWrapper wrapper = SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); 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