[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.');
}