[in_app_purchase]  Migrate to NNBD (#3555)

diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md
index abafaf5..79f64d5 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 d7b5973..e15cf14 100644
--- a/packages/in_app_purchase/build.yaml
+++ b/packages/in_app_purchase/build.yaml
@@ -5,4 +5,3 @@
         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 911edae..82cd509 100644
--- a/packages/in_app_purchase/example/lib/main.dart
+++ b/packages/in_app_purchase/example/lib/main.dart
@@ -32,7 +32,7 @@
 
 class _MyAppState extends State<_MyApp> {
   final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance;
-  StreamSubscription<List<PurchaseDetails>> _subscription;
+  late StreamSubscription<List<PurchaseDetails>> _subscription;
   List<String> _notFoundIds = [];
   List<ProductDetails> _products = [];
   List<PurchaseDetails> _purchases = [];
@@ -40,11 +40,11 @@
   bool _isAvailable = false;
   bool _purchasePending = false;
   bool _loading = true;
-  String _queryProductError;
+  String? _queryProductError;
 
   @override
   void initState() {
-    Stream purchaseUpdated =
+    final Stream<List<PurchaseDetails>> purchaseUpdated =
         InAppPurchaseConnection.instance.purchaseUpdatedStream;
     _subscription = purchaseUpdated.listen((purchaseDetailsList) {
       _listenToPurchaseUpdated(purchaseDetailsList);
@@ -76,7 +76,7 @@
         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 @@
       );
     } else {
       stack.add(Center(
-        child: Text(_queryProductError),
+        child: Text(_queryProductError!),
       ));
     }
     if (_purchasePending) {
@@ -235,7 +235,7 @@
     }));
     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 @@
                     onPressed: () {
                       PurchaseParam purchaseParam = PurchaseParam(
                           productDetails: productDetails,
-                          applicationUserName: null,
-                          sandboxTesting: true);
+                          applicationUserName: null);
                       if (productDetails.id == _kConsumableId) {
                         _connection.buyConsumable(
                             purchaseParam: purchaseParam,
@@ -329,7 +328,7 @@
   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<String> consumables = await ConsumableStore.load();
       setState(() {
         _purchasePending = false;
@@ -365,7 +364,7 @@
         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 9b623a1..8c9296d 100644
--- a/packages/in_app_purchase/example/pubspec.yaml
+++ b/packages/in_app_purchase/example/pubspec.yaml
@@ -5,11 +5,9 @@
 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 @@
     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 7a2c213..0352d4a 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 a5bfdb0..aa3430f 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 92872d9..f6bdf0c 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 <Flutter/Flutter.h>
 
diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
index 872a34a..9b44ad7 100644
--- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
+++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
@@ -75,7 +75,7 @@
       }];
   [_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 @@
   }];
 }
 
-#pragma mark - delegates
+#pragma mark - delegates:
 
 - (void)handleTransactionsUpdated:(NSArray<SKPaymentTransaction *> *)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 2aa91d9..9f96c05 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 @@
   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 @@
   /// 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<bool> isReady() async =>
-      await channel.invokeMethod<bool>('BillingClient#isReady()');
+  Future<bool> isReady() async {
+    final bool? ready =
+        await channel.invokeMethod<bool>('BillingClient#isReady()');
+    return ready ?? false;
+  }
 
   /// Enable the [BillingClientWrapper] to handle pending purchases.
   ///
@@ -100,20 +100,21 @@
   /// This triggers the creation of a new `BillingClient` instance in Java if
   /// one doesn't already exist.
   Future<BillingResultWrapper> startConnection(
-      {@required
-          OnBillingServiceDisconnected onBillingServiceDisconnected}) async {
+      {required OnBillingServiceDisconnected
+          onBillingServiceDisconnected}) async {
     assert(_enablePendingPurchases,
         'enablePendingPurchases() must be called before calling startConnection');
     List<Function> disconnectCallbacks =
         _callbacks[_kOnBillingServiceDisconnected] ??= [];
     disconnectCallbacks.add(onBillingServiceDisconnected);
-    return BillingResultWrapper.fromJson(await channel
-        .invokeMapMethod<String, dynamic>(
-            "BillingClient#startConnection(BillingClientStateListener)",
-            <String, dynamic>{
-          'handle': disconnectCallbacks.length - 1,
-          'enablePendingPurchases': _enablePendingPurchases
-        }));
+    return BillingResultWrapper.fromJson((await channel
+            .invokeMapMethod<String, dynamic>(
+                "BillingClient#startConnection(BillingClientStateListener)",
+                <String, dynamic>{
+              'handle': disconnectCallbacks.length - 1,
+              'enablePendingPurchases': _enablePendingPurchases
+            })) ??
+        <String, dynamic>{});
   }
 
   /// Calls
@@ -137,15 +138,16 @@
   /// `SkuDetailsParams` as direct arguments instead of requiring it constructed
   /// and passed in as a class.
   Future<SkuDetailsResponseWrapper> querySkuDetails(
-      {@required SkuType skuType, @required List<String> skusList}) async {
+      {required SkuType skuType, required List<String> skusList}) async {
     final Map<String, dynamic> arguments = <String, dynamic>{
       '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)) ??
+        <String, dynamic>{});
   }
 
   /// Attempt to launch the Play Billing Flow for a given [skuDetails].
@@ -172,16 +174,17 @@
   /// and [the given
   /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)).
   Future<BillingResultWrapper> launchBillingFlow(
-      {@required String sku, String accountId}) async {
+      {required String sku, String? accountId}) async {
     assert(sku != null);
     final Map<String, dynamic> arguments = <String, dynamic>{
       'sku': sku,
       'accountId': accountId,
     };
     return BillingResultWrapper.fromJson(
-        await channel.invokeMapMethod<String, dynamic>(
-            'BillingClient#launchBillingFlow(Activity, BillingFlowParams)',
-            arguments));
+        (await channel.invokeMapMethod<String, dynamic>(
+                'BillingClient#launchBillingFlow(Activity, BillingFlowParams)',
+                arguments)) ??
+            <String, dynamic>{});
   }
 
   /// Fetches recent purchases for the given [SkuType].
@@ -197,10 +200,12 @@
   /// skutype)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases).
   Future<PurchasesResultWrapper> queryPurchases(SkuType skuType) async {
     assert(skuType != null);
-    return PurchasesResultWrapper.fromJson(await channel
-        .invokeMapMethod<String, dynamic>(
-            'BillingClient#queryPurchases(String)',
-            <String, dynamic>{'skuType': SkuTypeConverter().toJson(skuType)}));
+    return PurchasesResultWrapper.fromJson((await channel
+            .invokeMapMethod<String, dynamic>(
+                'BillingClient#queryPurchases(String)', <String, dynamic>{
+          'skuType': SkuTypeConverter().toJson(skuType)
+        })) ??
+        <String, dynamic>{});
   }
 
   /// Fetches purchase history for the given [SkuType].
@@ -218,10 +223,13 @@
   /// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync).
   Future<PurchasesHistoryResult> queryPurchaseHistory(SkuType skuType) async {
     assert(skuType != null);
-    return PurchasesHistoryResult.fromJson(await channel.invokeMapMethod<String,
-            dynamic>(
-        'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)',
-        <String, dynamic>{'skuType': SkuTypeConverter().toJson(skuType)}));
+    return PurchasesHistoryResult.fromJson((await channel.invokeMapMethod<
+                String, dynamic>(
+            'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)',
+            <String, dynamic>{
+              'skuType': SkuTypeConverter().toJson(skuType)
+            })) ??
+        <String, dynamic>{});
   }
 
   /// Consumes a given in-app product.
@@ -229,20 +237,20 @@
   /// 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<BillingResultWrapper> consumeAsync(String purchaseToken,
-      {String developerPayload}) async {
+      {String? developerPayload}) async {
     assert(purchaseToken != null);
-    return BillingResultWrapper.fromJson(await channel
-        .invokeMapMethod<String, dynamic>(
-            'BillingClient#consumeAsync(String, ConsumeResponseListener)',
-            <String, String>{
-          'purchaseToken': purchaseToken,
-          'developerPayload': developerPayload,
-        }));
+    return BillingResultWrapper.fromJson((await channel
+            .invokeMapMethod<String, dynamic>(
+                'BillingClient#consumeAsync(String, ConsumeResponseListener)',
+                <String, dynamic>{
+              'purchaseToken': purchaseToken,
+              'developerPayload': developerPayload,
+            })) ??
+        <String, dynamic>{});
   }
 
   /// Acknowledge an in-app purchase.
@@ -261,20 +269,20 @@
   /// 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<BillingResultWrapper> acknowledgePurchase(String purchaseToken,
-      {String developerPayload}) async {
+      {String? developerPayload}) async {
     assert(purchaseToken != null);
-    return BillingResultWrapper.fromJson(await channel.invokeMapMethod<String,
-            dynamic>(
-        'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)',
-        <String, String>{
-          'purchaseToken': purchaseToken,
-          'developerPayload': developerPayload,
-        }));
+    return BillingResultWrapper.fromJson((await channel.invokeMapMethod<String,
+                dynamic>(
+            'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)',
+            <String, dynamic>{
+              'purchaseToken': purchaseToken,
+              'developerPayload': developerPayload,
+            })) ??
+        <String, dynamic>{});
   }
 
   /// The method call handler for [channel].
@@ -283,15 +291,15 @@
     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<String, dynamic>()));
         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 966c891..30828d8 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 @@
 ///
 /// Use these in `@JsonSerializable()` classes by annotating them with
 /// `@BillingResponseConverter()`.
-class BillingResponseConverter implements JsonConverter<BillingResponse, int> {
+class BillingResponseConverter implements JsonConverter<BillingResponse, int?> {
   /// Default const constructor.
   const BillingResponseConverter();
 
   @override
-  BillingResponse fromJson(int json) => _$enumDecode<BillingResponse>(
-      _$BillingResponseEnumMap.cast<BillingResponse, dynamic>(), json);
+  BillingResponse fromJson(int? json) {
+    if (json == null) {
+      return BillingResponse.error;
+    }
+    return _$enumDecode<BillingResponse, dynamic>(
+        _$BillingResponseEnumMap.cast<BillingResponse, dynamic>(), 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<SkuType, String> {
+class SkuTypeConverter implements JsonConverter<SkuType, String?> {
   /// Default const constructor.
   const SkuTypeConverter();
 
   @override
-  SkuType fromJson(String json) =>
-      _$enumDecode<SkuType>(_$SkuTypeEnumMap.cast<SkuType, dynamic>(), json);
+  SkuType fromJson(String? json) {
+    if (json == null) {
+      return SkuType.inapp;
+    }
+    return _$enumDecode<SkuType, dynamic>(
+        _$SkuTypeEnumMap.cast<SkuType, dynamic>(), 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 @@
 /// Use these in `@JsonSerializable()` classes by annotating them with
 /// `@PurchaseStateConverter()`.
 class PurchaseStateConverter
-    implements JsonConverter<PurchaseStateWrapper, int> {
+    implements JsonConverter<PurchaseStateWrapper, int?> {
   /// Default const constructor.
   const PurchaseStateConverter();
 
   @override
-  PurchaseStateWrapper fromJson(int json) => _$enumDecode<PurchaseStateWrapper>(
-      _$PurchaseStateWrapperEnumMap.cast<PurchaseStateWrapper, dynamic>(),
-      json);
+  PurchaseStateWrapper fromJson(int? json) {
+    if (json == null) {
+      return PurchaseStateWrapper.unspecified_state;
+    }
+    return _$enumDecode<PurchaseStateWrapper, dynamic>(
+        _$PurchaseStateWrapperEnumMap.cast<PurchaseStateWrapper, dynamic>(),
+        json);
+  }
 
   @override
   int toJson(PurchaseStateWrapper object) =>
-      _$PurchaseStateWrapperEnumMap[object];
+      _$PurchaseStateWrapperEnumMap[object]!;
 
   /// Converts the purchase state stored in `object` to a [PurchaseStatus].
   ///
@@ -78,7 +93,5 @@
       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 947700d..5d59dd8 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 @@
       'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState],
     };
 
-T _$enumDecode<T>(
-  Map<T, dynamic> enumValues,
-  dynamic source, {
-  T unknownValue,
+K _$enumDecode<K, V>(
+  Map<K, V> 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 8bdd738..0547227 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 @@
   /// 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<String, dynamic> 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 @@
 
   /// 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 @@
   ///
   /// 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 @@
   /// 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 @@
   /// 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<String, dynamic> 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 @@
   /// 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 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<String, dynamic> map) =>
@@ -215,7 +236,7 @@
   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 @@
   /// The list of successful purchases made in this transaction.
   ///
   /// May be empty, especially if [responseCode] is not [BillingResponse.ok].
+  @JsonKey(defaultValue: <PurchaseWrapper>[])
   final List<PurchaseWrapper> purchasesList;
 }
 
@@ -248,7 +270,7 @@
 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<String, dynamic> map) =>
@@ -258,7 +280,7 @@
   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 @@
   /// The list of queried purchase history records.
   ///
   /// May be empty, especially if [billingResult.responseCode] is not [BillingResponse.ok].
+  @JsonKey(defaultValue: <PurchaseHistoryRecordWrapper>[])
   final List<PurchaseHistoryRecordWrapper> 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 3d55589..5f0d936 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 @@
 
 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 @@
 
 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 @@
 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<dynamic>?)
+            ?.map((e) =>
+                PurchaseWrapper.fromJson(Map<String, dynamic>.from(e as Map)))
+            .toList() ??
+        [],
   );
 }
 
@@ -83,10 +88,16 @@
 
 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<dynamic>?)
+                ?.map((e) => PurchaseHistoryRecordWrapper.fromJson(
+                    Map<String, dynamic>.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 db65e20..b387295 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 @@
 // 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 @@
   /// 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 @@
   /// 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<String, dynamic> 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 @@
   /// 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 @@
   final BillingResultWrapper billingResult;
 
   /// A list of [SkuDetailsWrapper] matching the query to [BillingClient.querySkuDetails].
+  @JsonKey(defaultValue: <SkuDetailsWrapper>[])
   final List<SkuDetailsWrapper> skuDetailsList;
 
   @override
@@ -186,22 +209,29 @@
 @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<String, dynamic>? 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 70bde93..247dbd5 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 @@
 
 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 @@
 
 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<dynamic>?)
+            ?.map((e) =>
+                SkuDetailsWrapper.fromJson(Map<String, dynamic>.from(e as Map)))
+            .toList() ??
+        [],
   );
 }
 
@@ -66,8 +71,8 @@
 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 a0b92b5..5d140e2 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 a244ab1..50560a6 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 @@
   /// 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 @@
 
   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<bool> isAvailable() => SKPaymentQueueWrapper.canMakePayments();
 
   @override
-  Future<bool> buyNonConsumable({@required PurchaseParam purchaseParam}) async {
+  Future<bool> 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<bool> 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<BillingResultWrapper> 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<BillingResultWrapper> consumePurchase(PurchaseDetails purchase,
-      {String developerPayload}) {
+      {String? developerPayload}) {
     throw UnsupportedError('consume purchase is not available on Android');
   }
 
   @override
   Future<QueryPurchaseDetailsResponse> queryPastPurchases(
-      {String applicationUserName}) async {
-    IAPError error;
+      {String? applicationUserName}) async {
+    IAPError? error;
     List<PurchaseDetails> pastPurchases = [];
 
     try {
@@ -98,7 +104,6 @@
           await _observer.getRestoredTransactions(
               queue: _skPaymentQueueWrapper,
               applicationUserName: applicationUserName);
-      _observer.cleanUpRestoredTransactions();
       pastPurchases =
           restoredTransactions.map((SKPaymentTransactionWrapper transaction) {
         assert(transaction.transactionState ==
@@ -110,16 +115,17 @@
               ? 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 @@
   }
 
   @override
-  Future<PurchaseVerificationData> refreshPurchaseVerificationData() async {
+  Future<PurchaseVerificationData?> 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 @@
       Set<String> 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 @@
               ProductDetails.fromSKProduct(productWrapper))
           .toList();
     }
-    List<String> invalidIdentifiers = response.invalidProductIdentifiers ?? [];
+    List<String> invalidIdentifiers = response.invalidProductIdentifiers;
     if (productDetails.isEmpty) {
       invalidIdentifiers = identifiers.toList();
     }
@@ -179,7 +188,7 @@
           : IAPError(
               source: IAPSource.AppStore,
               code: exception.code,
-              message: exception.message,
+              message: exception.message ?? '',
               details: exception.details),
     );
     return productDetailsResponse;
@@ -189,27 +198,27 @@
 class _TransactionObserver implements SKTransactionObserverWrapper {
   final StreamController<List<PurchaseDetails>> purchaseUpdatedController;
 
-  Completer<List<SKPaymentTransactionWrapper>> _restoreCompleter;
-  List<SKPaymentTransactionWrapper> _restoredTransactions;
-  String _receiptData;
+  Completer<List<SKPaymentTransactionWrapper>>? _restoreCompleter;
+  List<SKPaymentTransactionWrapper> _restoredTransactions =
+      <SKPaymentTransactionWrapper>[];
+  late String _receiptData;
 
   _TransactionObserver(this.purchaseUpdatedController);
 
   Future<List<SKPaymentTransactionWrapper>> 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<SKPaymentTransactionWrapper> transactions}) async {
+      {required List<SKPaymentTransactionWrapper> transactions}) async {
     if (_restoreCompleter != null) {
       if (_restoredTransactions == null) {
         _restoredTransactions = [];
@@ -233,19 +242,20 @@
     }).toList());
   }
 
-  void removedTransactions({List<SKPaymentTransactionWrapper> transactions}) {}
+  void removedTransactions(
+      {required List<SKPaymentTransactionWrapper> 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 @@
     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 b980bbd..ef0b7d2 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/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 @@
       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<List<PurchaseDetails>> get purchaseUpdatedStream =>
       _purchaseUpdatedController.stream;
-  static StreamController<List<PurchaseDetails>> _purchaseUpdatedController;
+  static late StreamController<List<PurchaseDetails>>
+      _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<void> _readyFuture;
+  late Future<void> _readyFuture;
   static Set<String> _productIdsToConsume = Set<String>();
 
   @override
@@ -57,7 +59,7 @@
   }
 
   @override
-  Future<bool> buyNonConsumable({@required PurchaseParam purchaseParam}) async {
+  Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) async {
     BillingResultWrapper billingResultWrapper =
         await billingClient.launchBillingFlow(
             sku: purchaseParam.productDetails.id,
@@ -67,7 +69,7 @@
 
   @override
   Future<bool> buyConsumable(
-      {@required PurchaseParam purchaseParam, bool autoConsume = true}) {
+      {required PurchaseParam purchaseParam, bool autoConsume = true}) {
     if (autoConsume) {
       _productIdsToConsume.add(purchaseParam.productDetails.id);
     }
@@ -76,10 +78,14 @@
 
   @override
   Future<BillingResultWrapper> 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 @@
 
   @override
   Future<BillingResultWrapper> 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 @@
 
   @override
   Future<QueryPurchaseDetailsResponse> queryPastPurchases(
-      {String applicationUserName}) async {
+      {String? applicationUserName}) async {
     List<PurchasesResultWrapper> responses;
-    PlatformException exception;
+    PlatformException? exception;
     try {
       responses = await Future.wait([
         billingClient.queryPurchases(SkuType.inapp),
@@ -133,7 +143,7 @@
         .toSet();
 
     String errorMessage =
-        errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : null;
+        errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : '';
 
     List<PurchaseDetails> pastPurchases =
         responses.expand((PurchasesResultWrapper response) {
@@ -142,14 +152,14 @@
       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 @@
 
   static GooglePlayConnection _getOrCreateInstance() {
     if (_instance != null) {
-      return _instance;
+      return _instance!;
     }
 
     _instance = GooglePlayConnection._();
-    return _instance;
+    return _instance!;
   }
 
   Future<void> _connect() =>
@@ -193,7 +203,7 @@
   Future<ProductDetailsResponse> queryProductDetails(
       Set<String> identifiers) async {
     List<SkuDetailsResponseWrapper> responses;
-    PlatformException exception;
+    PlatformException? exception;
     try {
       responses = await Future.wait([
         billingClient.querySkuDetails(
@@ -235,13 +245,13 @@
             : IAPError(
                 source: IAPSource.GooglePlay,
                 code: exception.code,
-                message: exception.message,
+                message: exception.message ?? '',
                 details: exception.details));
   }
 
   static Future<List<PurchaseDetails>> _getPurchaseDetailsFromResult(
       PurchasesResultWrapper resultWrapper) async {
-    IAPError error;
+    IAPError? error;
     if (resultWrapper.responseCode != BillingResponse.ok) {
       error = IAPError(
         source: IAPSource.GooglePlay,
@@ -260,10 +270,13 @@
     } 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 f07ff96..81a0e92 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 '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 @@
   /// events after they start to listen.
   Stream<List<PurchaseDetails>> get purchaseUpdatedStream => _getStream();
 
-  Stream<List<PurchaseDetails>> _purchaseUpdatedStream;
+  Stream<List<PurchaseDetails>>? _purchaseUpdatedStream;
 
   Stream<List<PurchaseDetails>> _getStream() {
     if (_purchaseUpdatedStream != null) {
-      return _purchaseUpdatedStream;
+      return _purchaseUpdatedStream!;
     }
 
     if (Platform.isAndroid) {
@@ -57,7 +56,7 @@
       throw UnsupportedError(
           'InAppPurchase plugin only works on Android and iOS.');
     }
-    return _purchaseUpdatedStream;
+    return _purchaseUpdatedStream!;
   }
 
   /// Whether pending purchase is enabled.
@@ -133,7 +132,7 @@
   ///  * [queryPastPurchases], for restoring non consumable products.
   ///
   /// Calling this method for consumable items will cause unwanted behaviors!
-  Future<bool> buyNonConsumable({@required PurchaseParam purchaseParam});
+  Future<bool> buyNonConsumable({required PurchaseParam purchaseParam});
 
   /// Buy a consumable product.
   ///
@@ -186,7 +185,7 @@
   /// Calling this method for non consumable items will cause unwanted
   /// behaviors!
   Future<bool> 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 @@
   /// 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<BillingResultWrapper> completePurchase(PurchaseDetails purchase,
-      {String developerPayload});
+      {String? developerPayload});
 
   /// (Play only) Mark that the user has consumed a product.
   ///
@@ -216,16 +215,17 @@
   /// 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<BillingResultWrapper> 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 @@
   ///  * [refreshPurchaseVerificationData], for reloading failed
   ///    [PurchaseDetails.verificationData].
   Future<QueryPurchaseDetailsResponse> 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<PurchaseVerificationData> refreshPurchaseVerificationData();
+  Future<PurchaseVerificationData?> 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 @@
           'InAppPurchase plugin only works on Android and iOS.');
     }
 
-    return _instance;
+    return _instance!;
   }
 }
 
@@ -287,9 +289,9 @@
 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 @@
   /// 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 bb9e268..a3eb79d 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 @@
 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 @@
 
   /// 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 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> productDetails;
@@ -82,7 +81,9 @@
 
   /// 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 708b42c..c211d2a 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 @@
 
   /// 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 @@
 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 @@
   /// 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 @@
 /// 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 @@
   /// 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 @@
     _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 @@
   // 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 @@
             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 @@
       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 @@
       error = IAPError(
         source: IAPSource.GooglePlay,
         code: kPurchaseErrorCode,
-        message: null,
+        message: '',
       );
     }
   }
@@ -246,7 +261,7 @@
 /// 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 @@
 
   /// 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 6218870..ce2c1fa 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 @@
 /// Use these in `@JsonSerializable()` classes by annotating them with
 /// `@SKTransactionStatusConverter()`.
 class SKTransactionStatusConverter
-    implements JsonConverter<SKPaymentTransactionStateWrapper, int> {
+    implements JsonConverter<SKPaymentTransactionStateWrapper, int?> {
   /// Default const constructor.
   const SKTransactionStatusConverter();
 
   @override
-  SKPaymentTransactionStateWrapper fromJson(int json) =>
-      _$enumDecode<SKPaymentTransactionStateWrapper>(
-          _$SKPaymentTransactionStateWrapperEnumMap
-              .cast<SKPaymentTransactionStateWrapper, dynamic>(),
-          json);
+  SKPaymentTransactionStateWrapper fromJson(int? json) {
+    if (json == null) {
+      return SKPaymentTransactionStateWrapper.unspecified;
+    }
+    return _$enumDecode<SKPaymentTransactionStateWrapper, dynamic>(
+        _$SKPaymentTransactionStateWrapperEnumMap
+            .cast<SKPaymentTransactionStateWrapper, dynamic>(),
+        json);
+  }
 
   /// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus].
   PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) {
@@ -34,19 +38,70 @@
       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<SKSubscriptionPeriodUnit, int?> {
+  /// Default const constructor.
+  const SKSubscriptionPeriodUnitConverter();
+
+  @override
+  SKSubscriptionPeriodUnit fromJson(int? json) {
+    if (json == null) {
+      return SKSubscriptionPeriodUnit.day;
+    }
+    return _$enumDecode<SKSubscriptionPeriodUnit, dynamic>(
+        _$SKSubscriptionPeriodUnitEnumMap
+            .cast<SKSubscriptionPeriodUnit, dynamic>(),
+        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<SKProductDiscountPaymentMode, int?> {
+  /// Default const constructor.
+  const SKProductDiscountPaymentModeConverter();
+
+  @override
+  SKProductDiscountPaymentMode fromJson(int? json) {
+    if (json == null) {
+      return SKProductDiscountPaymentMode.payAsYouGo;
+    }
+    return _$enumDecode<SKProductDiscountPaymentMode, dynamic>(
+        _$SKProductDiscountPaymentModeEnumMap
+            .cast<SKProductDiscountPaymentMode, dynamic>(),
+        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 f4f17df..b003f43 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 @@
 _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<String, dynamic> _$_SerializedEnumsToJson(_SerializedEnums instance) =>
     <String, dynamic>{
       'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response],
+      'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit],
+      'discountPaymentMode':
+          _$SKProductDiscountPaymentModeEnumMap[instance.discountPaymentMode],
     };
 
-T _$enumDecode<T>(
-  Map<T, dynamic> enumValues,
-  dynamic source, {
-  T unknownValue,
+K _$enumDecode<K, V>(
+  Map<K, V> 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 @@
   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 ce38759..d56fbd0 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 @@
 /// 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 @@
 
   /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc)
   Future<List<SKPaymentTransactionWrapper>> transactions() async {
-    return _getTransactionList(
-        await channel.invokeListMethod<Map>('-[SKPaymentQueue transactions]'));
+    return _getTransactionList((await channel
+        .invokeListMethod<dynamic>('-[SKPaymentQueue transactions]'))!);
   }
 
   /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc).
   static Future<bool> canMakePayments() async =>
-      await channel.invokeMethod<bool>('-[SKPaymentQueue canMakePayments:]');
+      (await channel
+          .invokeMethod<bool>('-[SKPaymentQueue canMakePayments:]')) ??
+      false;
 
   /// Sets an observer to listen to all incoming transaction events.
   ///
@@ -57,7 +58,7 @@
   /// 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 @@
   Future<void> 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<String, dynamic> requestMap = payment.toMap();
     await channel.invokeMethod<void>(
       '-[InAppPurchasePlugin addPayment:result:]',
       requestMap,
@@ -103,7 +104,7 @@
   /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc).
   Future<void> finishTransaction(
       SKPaymentTransactionWrapper transaction) async {
-    Map<String, String> requestMap = transaction.toFinishMap();
+    Map<String, String?> requestMap = transaction.toFinishMap();
     await channel.invokeMethod<void>(
       '-[InAppPurchasePlugin finishTransaction:result:]',
       requestMap,
@@ -124,28 +125,30 @@
   ///
   /// 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<void> restoreTransactions({String applicationUserName}) async {
+  Future<void> restoreTransactions({String? applicationUserName}) async {
     await channel.invokeMethod<void>(
         '-[InAppPurchasePlugin restoreTransactions:result:]',
         applicationUserName);
   }
 
   // Triage a method channel call from the platform and triggers the correct observer method.
-  Future<dynamic> _handleObserverCallbacks(MethodCall call) {
+  Future<void> _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<SKPaymentTransactionWrapper> transactions =
               _getTransactionList(call.arguments);
           return Future<void>(() {
-            _observer.updatedTransactions(transactions: transactions);
+            observer.updatedTransactions(transactions: transactions);
           });
         }
       case 'removedTransactions':
@@ -153,20 +156,20 @@
           final List<SKPaymentTransactionWrapper> transactions =
               _getTransactionList(call.arguments);
           return Future<void>(() {
-            _observer.removedTransactions(transactions: transactions);
+            observer.removedTransactions(transactions: transactions);
           });
         }
       case 'restoreCompletedTransactionsFailed':
         {
           SKError error = SKError.fromJson(call.arguments);
           return Future<void>(() {
-            _observer.restoreCompletedTransactionsFailed(error: error);
+            observer.restoreCompletedTransactionsFailed(error: error);
           });
         }
       case 'paymentQueueRestoreCompletedTransactionsFinished':
         {
           return Future<void>(() {
-            _observer.paymentQueueRestoreCompletedTransactionsFinished();
+            observer.paymentQueueRestoreCompletedTransactionsFinished();
           });
         }
       case 'shouldAddStorePayment':
@@ -176,7 +179,7 @@
           SKProductWrapper product =
               SKProductWrapper.fromJson(call.arguments['product']);
           return Future<void>(() {
-            if (_observer.shouldAddStorePayment(
+            if (observer.shouldAddStorePayment(
                     payment: payment, product: product) ==
                 true) {
               SKPaymentQueueWrapper().addPayment(payment);
@@ -186,49 +189,52 @@
       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<SKPaymentTransactionWrapper> _getTransactionList(dynamic arguments) {
-    final List<SKPaymentTransactionWrapper> transactions = arguments
-        .map<SKPaymentTransactionWrapper>(
-            (dynamic map) => SKPaymentTransactionWrapper.fromJson(map))
-        .toList();
-    return transactions;
+  List<SKPaymentTransactionWrapper> _getTransactionList(
+      List<dynamic> transactionsData) {
+    return transactionsData.map<SKPaymentTransactionWrapper>((dynamic map) {
+      return SKPaymentTransactionWrapper.fromJson(
+          Map.castFrom<dynamic, dynamic, String, dynamic>(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<String, dynamic> 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: <String, dynamic>{})
   final Map<String, dynamic> userInfo;
 
   @override
@@ -239,7 +245,7 @@
     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 @@
 /// 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 @@
   /// 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<String, dynamic> map) {
     assert(map != null);
     return _$SKPaymentWrapperFromJson(map);
   }
@@ -289,6 +295,7 @@
   }
 
   /// 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 @@
   /// 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 @@
   // 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 @@
     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 48a18e6..2b88659 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 @@
 
 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 @@
 
 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 65f6ff8..9921380 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 @@
 /// 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<SKPaymentTransactionWrapper> transactions});
+  void updatedTransactions(
+      {required List<SKPaymentTransactionWrapper> transactions});
 
   /// Triggered when any transactions are removed from the payment queue.
-  void removedTransactions({List<SKPaymentTransactionWrapper> transactions});
+  void removedTransactions(
+      {required List<SKPaymentTransactionWrapper> 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 @@
   /// 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 @@
   /// 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 @@
 ///
 /// 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 @@
   /// 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<String, dynamic> map) {
     return _$SKPaymentTransactionWrapperFromJson(map);
   }
 
@@ -130,18 +132,21 @@
 
   /// 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 @@
   /// [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 @@
     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 @@
   String toString() => _$SKPaymentTransactionWrapperToJson(this).toString();
 
   /// The payload that is used to finish this transaction.
-  Map<String, String> toFinishMap() => {
+  Map<String, String?> toFinishMap() => <String, String?>{
         "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 bc52082..4c7af21 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 @@
 
 SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) {
   return SKPaymentTransactionWrapper(
-    payment: json['payment'] == null
-        ? null
-        : SKPaymentWrapper.fromJson(json['payment'] as Map),
+    payment: SKPaymentWrapper.fromJson(
+        Map<String, dynamic>.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<String, dynamic>.from(json['originalTransaction'] as Map)),
+    transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(),
+    transactionIdentifier: json['transactionIdentifier'] as String?,
+    error: json['error'] == null
+        ? null
+        : SKError.fromJson(Map<String, dynamic>.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 aa76971..d77ea81 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 @@
 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<String, dynamic> map) {
-    assert(map != null, 'Map must not be null.');
     return _$SkProductResponseWrapperFromJson(map);
   }
 
@@ -35,6 +33,7 @@
   ///
   /// 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: <SKProductWrapper>[])
   final List<SKProductWrapper> products;
 
   /// Stores product identifiers in the `productIdentifiers` from [SKRequestMaker.startProductRequest] that are not recognized by the App Store.
@@ -42,6 +41,7 @@
   /// 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: <String>[])
   final List<String> invalidProductIdentifiers;
 
   @override
@@ -52,7 +52,8 @@
     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 @@
 ///
 /// 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<String, dynamic>? 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 @@
     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 @@
   /// 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<String, dynamic> 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 @@
 
   /// 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 @@
     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 @@
 ///
 /// 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<String, dynamic> 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 @@
 
   /// 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 @@
     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 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<String, dynamic>? 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 @@
     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 cf27852..8c2eed3 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 @@
 
 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<dynamic>?)
+            ?.map((e) =>
+                SKProductWrapper.fromJson(Map<String, dynamic>.from(e as Map)))
+            .toList() ??
+        [],
+    invalidProductIdentifiers:
+        (json['invalidProductIdentifiers'] as List<dynamic>?)
+                ?.map((e) => e as String)
+                .toList() ??
+            [],
   );
 }
 
@@ -27,8 +31,9 @@
 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 @@
         SKProductSubscriptionPeriodWrapper instance) =>
     <String, dynamic>{
       'numberOfUnits': instance.numberOfUnits,
-      'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit],
+      'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit),
     };
 
-T _$enumDecode<T>(
-  Map<T, dynamic> 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<T>(
-  Map<T, dynamic> enumValues,
-  dynamic source, {
-  T unknownValue,
-}) {
-  if (source == null) {
-    return null;
-  }
-  return _$enumDecode<T>(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 @@
       '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<String, dynamic>.from(json['introductoryPrice'] as Map)),
   );
 }
 
@@ -145,8 +110,8 @@
 
 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 85af9de..16bcb77 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 @@
   /// 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<String> retrieveReceiptData() {
-    return channel.invokeMethod<String>(
-        '-[InAppPurchasePlugin retrieveReceiptData:result:]');
+  static Future<String> retrieveReceiptData() async {
+    return (await channel.invokeMethod<String>(
+            '-[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 959113c..c22df0a 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 @@
   /// A [PlatformException] is thrown if the platform code making the request fails.
   Future<SkProductResponseWrapper> startProductRequest(
       List<String> productIdentifiers) async {
-    final Map<String, dynamic> productResponseMap =
+    final Map<String, dynamic>? productResponseMap =
         await channel.invokeMapMethod<String, dynamic>(
       '-[InAppPurchasePlugin startProductRequest:result:]',
       productIdentifiers,
@@ -47,7 +47,8 @@
   /// * isExpired: whether the receipt is expired.
   /// * isRevoked: whether the receipt has been revoked.
   /// * isVolumePurchase: whether the receipt is a Volume Purchase Plan receipt.
-  Future<void> startRefreshReceiptRequest({Map receiptProperties}) {
+  Future<void> startRefreshReceiptRequest(
+      {Map<String, dynamic>? receiptProperties}) {
     return channel.invokeMethod<void>(
       '-[InAppPurchasePlugin refreshReceipt:result:]',
       receiptProperties,
diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml
index 6a6c525..f847a81 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 @@
         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 eee33a6..d415007 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 @@
   TestWidgetsFlutterBinding.ensureInitialized();
 
   final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform();
-  BillingClient billingClient;
+  late BillingClient billingClient;
 
   setUpAll(() =>
       channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler));
@@ -96,6 +96,20 @@
           equals(
               <dynamic, dynamic>{'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 @@
       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: <String>['invalid']);
+
+      BillingResultWrapper billingResult = BillingResultWrapper(
+          responseCode: BillingResponse.error,
+          debugMessage: kInvalidBillingResultErrorMessage);
+      expect(response.billingResult, equals(billingResult));
+      expect(response.skuDetailsList, isEmpty);
+    });
   });
 
   group('launchBillingFlow', () {
@@ -197,6 +225,19 @@
       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 @@
       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 @@
       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 @@
       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 @@
       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 @@
 
       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 @@
 
       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 978252a..7f3de27 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 @@
       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 @@
       expect(parsed.responseCode, equals(expected.responseCode));
       expect(parsed.purchasesList, containsAll(expected.purchasesList));
     });
+
+    test('parsed from empty map', () {
+      final PurchasesResultWrapper parsed =
+          PurchasesResultWrapper.fromJson(<String, dynamic>{});
+      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 @@
       expect(parsed.purchaseHistoryRecordList,
           containsAll(expected.purchaseHistoryRecordList));
     });
+
+    test('parsed from empty map', () {
+      final PurchasesHistoryResult parsed =
+          PurchasesHistoryResult.fromJson(<String, dynamic>{});
+      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 c305e6d..13715ee 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 @@
       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(<String, dynamic>{});
+      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(<String, dynamic>{});
+      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 b22737c..bfcab08 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/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 @@
           .queryProductDetails(<String>['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 @@
           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 @@
       Stream<List<PurchaseDetails>> 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 @@
       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 @@
           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 @@
       Stream<List<PurchaseDetails>> 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 @@
       Stream<List<PurchaseDetails>> 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 @@
       fakeIOSPlatform.testTransactionFail = true;
       List<PurchaseDetails> details = [];
       Completer completer = Completer();
-      IAPError error;
+      late IAPError error;
 
       Stream<List<PurchaseDetails>> 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 @@
       Completer completer = Completer();
       Stream<List<PurchaseDetails>> stream =
           AppStoreConnection.instance.purchaseUpdatedStream;
-      StreamSubscription subscription;
+      late StreamSubscription subscription;
       subscription = stream.listen((purchaseDetailsList) {
         details.addAll(purchaseDetailsList);
         purchaseDetailsList.forEach((purchaseDetails) {
@@ -288,7 +294,9 @@
 
   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 @@
   }
 
   // pre-configured store informations
-  String receiptData;
-  Set<String> validProductIDs;
-  Map<String, SKProductWrapper> validProducts;
-  List<SKPaymentTransactionWrapper> transactions;
-  List<SKPaymentTransactionWrapper> finishedTransactions;
-  bool testRestoredTransactionsNull;
-  bool testTransactionFail;
-  PlatformException queryProductException;
-  PlatformException restoreException;
-  SKError testRestoredError;
+  String? receiptData;
+  late Set<String> validProductIDs;
+  late Map<String, SKProductWrapper> validProducts;
+  late List<SKPaymentTransactionWrapper> transactions;
+  late List<SKPaymentTransactionWrapper> finishedTransactions;
+  late bool testRestoredTransactionsNull;
+  late bool testTransactionFail;
+  PlatformException? queryProductException;
+  PlatformException? restoreException;
+  SKError? testRestoredError;
 
   void reset() {
     transactions = [];
@@ -317,7 +325,8 @@
     validProductIDs = ['123', '456'].toSet();
     validProducts = Map();
     for (String validID in validProductIDs) {
-      Map productWrapperMap = buildProductMap(dummyProductWrapper);
+      Map<String, dynamic> productWrapperMap =
+          buildProductMap(dummyProductWrapper);
       productWrapperMap['productIdentifier'] = validID;
       validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap);
     }
@@ -350,7 +359,7 @@
 
   SKPaymentTransactionWrapper createPendingTransaction(String id) {
     return SKPaymentTransactionWrapper(
-        transactionIdentifier: null,
+        transactionIdentifier: '',
         payment: SKPaymentWrapper(productIdentifier: id),
         transactionState: SKPaymentTransactionStateWrapper.purchasing,
         transactionTimeStamp: 123123.121,
@@ -371,7 +380,7 @@
 
   SKPaymentTransactionWrapper createFailedTransaction(String productId) {
     return SKPaymentTransactionWrapper(
-        transactionIdentifier: null,
+        transactionIdentifier: '',
         payment: SKPaymentWrapper(productIdentifier: productId),
         transactionState: SKPaymentTransactionStateWrapper.failed,
         transactionTimeStamp: 123123.121,
@@ -388,7 +397,7 @@
         return Future<bool>.value(true);
       case '-[InAppPurchasePlugin startProductRequest:result:]':
         if (queryProductException != null) {
-          throw queryProductException;
+          throw queryProductException!;
         }
         List<String> productIDS =
             List.castFrom<dynamic, String>(call.arguments);
@@ -399,7 +408,7 @@
           if (!validProductIDs.contains(productID)) {
             invalidFound.add(productID);
           } else {
-            products.add(validProducts[productID]);
+            products.add(validProducts[productID]!);
           }
         }
         SkProductResponseWrapper response = SkProductResponseWrapper(
@@ -408,11 +417,11 @@
             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<void>.sync(() {});
         }
         if (!testRestoredTransactionsNull) {
@@ -428,7 +437,6 @@
         } else {
           throw PlatformException(code: 'no_receipt_data');
         }
-        break;
       case '-[InAppPurchasePlugin refreshReceipt:result:]':
         receiptData = 'refreshed receipt data';
         return Future<void>.sync(() {});
@@ -445,7 +453,8 @@
               .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 9294d2b..79c2ee4 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 @@
   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 @@
           await connection.queryProductDetails(<String>['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 @@
       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 @@
       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 @@
       PurchaseDetails purchaseDetails;
       Stream purchaseStream =
           GooglePlayConnection.instance.purchaseUpdatedStream;
-      StreamSubscription subscription;
+      late StreamSubscription subscription;
       subscription = purchaseStream.listen((_) {
         purchaseDetails = _.first;
         completer.complete(purchaseDetails);
@@ -320,7 +324,7 @@
       PurchaseDetails purchaseDetails;
       Stream purchaseStream =
           GooglePlayConnection.instance.purchaseUpdatedStream;
-      StreamSubscription subscription;
+      late StreamSubscription subscription;
       subscription = purchaseStream.listen((_) {
         purchaseDetails = _.first;
         completer.complete(purchaseDetails);
@@ -334,9 +338,9 @@
       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 @@
       PurchaseDetails purchaseDetails;
       Stream purchaseStream =
           GooglePlayConnection.instance.purchaseUpdatedStream;
-      StreamSubscription subscription;
+      late StreamSubscription subscription;
       subscription = purchaseStream.listen((_) {
         purchaseDetails = _.first;
         completer.complete(purchaseDetails);
@@ -407,7 +411,8 @@
       // 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 @@
       PurchaseDetails purchaseDetails;
       Stream purchaseStream =
           GooglePlayConnection.instance.purchaseUpdatedStream;
-      StreamSubscription subscription;
+      late StreamSubscription subscription;
       subscription = purchaseStream.listen((_) {
         purchaseDetails = _.first;
         completer.complete(purchaseDetails);
@@ -515,11 +520,12 @@
 
       // 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 @@
 
       Stream purchaseStream =
           GooglePlayConnection.instance.purchaseUpdatedStream;
-      StreamSubscription subscription;
+      late StreamSubscription subscription;
       subscription = purchaseStream.listen((_) {
         consumeCompleter.complete(null);
         subscription.cancel();
@@ -629,10 +635,6 @@
             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 92ffbc5..d41a126 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 @@
 
   setUp(() {});
 
+  tearDown(() {
+    fakeIOSPlatform.testReturnNull = false;
+  });
+
   group('sk_request_maker', () {
     test('get products method channel', () async {
       SkProductResponseWrapper productResponseWrapper =
@@ -55,7 +59,7 @@
     test('get products method channel should throw exception', () async {
       fakeIOSPlatform.getProductRequestFailTest = true;
       expect(
-        SKRequestMaker().startProductRequest(['xxx']),
+        SKRequestMaker().startProductRequest(<String>['xxx']),
         throwsException,
       );
       fakeIOSPlatform.getProductRequestFailTest = false;
@@ -63,10 +67,11 @@
 
     test('refreshed receipt', () async {
       int receiptCountBefore = fakeIOSPlatform.refreshReceipt;
-      await SKRequestMaker()
-          .startRefreshReceiptRequest(receiptProperties: {"isExpired": true});
+      await SKRequestMaker().startRefreshReceiptRequest(
+          receiptProperties: <String, dynamic>{"isExpired": true});
       expect(fakeIOSPlatform.refreshReceipt, receiptCountBefore + 1);
-      expect(fakeIOSPlatform.refreshReceiptParam, {"isExpired": true});
+      expect(fakeIOSPlatform.refreshReceiptParam,
+          <String, dynamic>{"isExpired": true});
     });
   });
 
@@ -83,6 +88,12 @@
       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 @@
 class FakeIOSPlatform {
   FakeIOSPlatform() {
     channel.setMockMethodCallHandler(onMethodCall);
-    getProductRequestFailTest = false;
   }
   // get product request
-  List startProductRequestParam;
-  bool getProductRequestFailTest;
+  List<dynamic> startProductRequestParam = [];
+  bool getProductRequestFailTest = false;
+  bool testReturnNull = false;
 
   // refresh receipt request
   int refreshReceipt = 0;
-  Map refreshReceiptParam;
+  late Map<String, dynamic> refreshReceiptParam;
 
   // payment queue
   List<SKPaymentWrapper> payments = [];
   List<Map<String, String>> transactionsFinished = [];
-  String applicationNameHasTransactionRestored;
+  String applicationNameHasTransactionRestored = '';
 
   Future<dynamic> onMethodCall(MethodCall call) {
     switch (call.method) {
@@ -157,18 +168,24 @@
             buildProductResponseMap(dummyProductResponseWrapper));
       case '-[InAppPurchasePlugin refreshReceipt:result:]':
         refreshReceipt++;
-        refreshReceiptParam = call.arguments;
+        refreshReceiptParam =
+            Map.castFrom<dynamic, dynamic, String, dynamic>(call.arguments);
         return Future<void>.sync(() {});
       // receipt manager
       case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
         return Future<String>.value('receipt data');
       // payment queue
       case '-[SKPaymentQueue canMakePayments:]':
+        if (testReturnNull) {
+          return Future<dynamic>.value(null);
+        }
         return Future<bool>.value(true);
       case '-[SKPaymentQueue transactions]':
-        return Future<List<Map>>.value([buildTransactionMap(dummyTransaction)]);
+        return Future<List<dynamic>>.value(
+            [buildTransactionMap(dummyTransaction)]);
       case '-[InAppPurchasePlugin addPayment:result:]':
-        payments.add(SKPaymentWrapper.fromJson(call.arguments));
+        payments.add(SKPaymentWrapper.fromJson(
+            Map<String, dynamic>.from(call.arguments)));
         return Future<void>.sync(() {});
       case '-[InAppPurchasePlugin finishTransaction:result:]':
         transactionsFinished.add(Map<String, String>.from(call.arguments));
@@ -182,16 +199,18 @@
 }
 
 class TestPaymentTransactionObserver extends SKTransactionObserverWrapper {
-  void updatedTransactions({List<SKPaymentTransactionWrapper> transactions}) {}
+  void updatedTransactions(
+      {required List<SKPaymentTransactionWrapper> transactions}) {}
 
-  void removedTransactions({List<SKPaymentTransactionWrapper> transactions}) {}
+  void removedTransactions(
+      {required List<SKPaymentTransactionWrapper> 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 2a9066f..6e1f59b 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 @@
         () {
       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(<String, dynamic>{});
-      expect(wrapper.numberOfUnits, null);
-      expect(wrapper.unit, null);
+      expect(wrapper.numberOfUnits, 0);
+      expect(wrapper.unit, SKSubscriptionPeriodUnit.day);
     });
 
     test(
@@ -39,15 +39,19 @@
     });
 
     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(<String, dynamic>{});
-      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 @@
       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(<String, dynamic>{});
-      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 @@
           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 @@
       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<String, String?> 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<String, String?> 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 c976e80..f7d86f5 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<String, dynamic> buildSubscriptionPeriodMap(
-    SKProductSubscriptionPeriodWrapper sub) {
+Map<String, dynamic>? buildSubscriptionPeriodMap(
+    SKProductSubscriptionPeriodWrapper? sub) {
+  if (sub == null) {
+    return null;
+  }
   return {
     'numberOfUnits': sub.numberOfUnits,
     'unit': SKSubscriptionPeriodUnit.values.indexOf(sub.unit),
@@ -104,7 +107,7 @@
     'price': product.price,
     'subscriptionPeriod':
         buildSubscriptionPeriodMap(product.subscriptionPeriod),
-    'introductoryPrice': buildDiscountMap(product.introductoryPrice),
+    'introductoryPrice': buildDiscountMap(product.introductoryPrice!),
   };
 }
 
@@ -129,17 +132,16 @@
 
 Map<String, dynamic> buildTransactionMap(
     SKPaymentTransactionWrapper transaction) {
-  if (transaction == null) {
-    return null;
-  }
-  Map map = <String, dynamic>{
+  Map<String, dynamic> map = <String, dynamic>{
     '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 3124795..431d885 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 @@
 
 class StubInAppPurchasePlatform {
   Map<String, dynamic> _expectedCalls = <String, dynamic>{};
-  Map<String, AdditionalSteps> _additionalSteps = <String, AdditionalSteps>{};
+  Map<String, AdditionalSteps?> _additionalSteps = <String, AdditionalSteps?>{};
   void addResponse(
-      {String name,
+      {required String name,
       dynamic value,
-      AdditionalSteps additionalStepBeforeReturn}) {
+      AdditionalSteps? additionalStepBeforeReturn}) {
     _additionalSteps[name] = additionalStepBeforeReturn;
     _expectedCalls[name] = value;
   }
 
   List<MethodCall> _previousCalls = <MethodCall>[];
   List<MethodCall> 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 @@
     _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<dynamic>.sync(() => _expectedCalls[call.method]);
     } else {
diff --git a/script/nnbd_plugins.sh b/script/nnbd_plugins.sh
index 3e9e50e..eceb78c 100644
--- a/script/nnbd_plugins.sh
+++ b/script/nnbd_plugins.sh
@@ -32,6 +32,7 @@
   "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 af00995..670feda 100644
--- a/script/tool/lib/src/publish_check_command.dart
+++ b/script/tool/lib/src/publish_check_command.dart
@@ -97,7 +97,7 @@
     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.');
   }