[in_app_purchase] Fix finishing purchases upon payment cancellation (#3106)
diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md
index f1b728b..a436a5e 100644
--- a/packages/in_app_purchase/CHANGELOG.md
+++ b/packages/in_app_purchase/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 0.3.4+12
+
+* [iOS] Fixed: finishing purchases upon payment dialog cancellation.
## 0.3.4+11
diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
index 6bcd58e..872a34a 100644
--- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
+++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
@@ -199,19 +199,27 @@
}
- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result {
- if (![call.arguments isKindOfClass:[NSString class]]) {
+ if (![call.arguments isKindOfClass:[NSDictionary class]]) {
result([FlutterError errorWithCode:@"storekit_invalid_argument"
- message:@"Argument type of finishTransaction is not a string."
+ message:@"Argument type of finishTransaction is not a Dictionary"
details:call.arguments]);
return;
}
- NSString *transactionIdentifier = call.arguments;
+ NSDictionary *paymentMap = (NSDictionary *)call.arguments;
+ NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"];
+ NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"];
NSArray<SKPaymentTransaction *> *pendingTransactions =
[self.paymentQueueHandler getUnfinishedTransactions];
for (SKPaymentTransaction *transaction in pendingTransactions) {
- if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier]) {
+ // If the user cancels the purchase dialog we won't have a transactionIdentifier.
+ // So if it is null AND a transaction in the pendingTransactions list has
+ // also a null transactionIdentifier we check for equal product identifiers.
+ if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier] ||
+ ([transactionIdentifier isEqual:[NSNull null]] &&
+ transaction.transactionIdentifier == nil &&
+ [transaction.payment.productIdentifier isEqualToString:productIdentifier])) {
@try {
[self.paymentQueueHandler finishTransaction:transaction];
} @catch (NSException *e) {
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 7f20736..bb731ac 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
@@ -103,9 +103,11 @@
/// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc).
Future<void> finishTransaction(
SKPaymentTransactionWrapper transaction) async {
+ Map<String, String> requestMap = transaction.toFinishMap();
await channel.invokeMethod<void>(
- '-[InAppPurchasePlugin finishTransaction:result:]',
- transaction.transactionIdentifier);
+ '-[InAppPurchasePlugin finishTransaction:result:]',
+ requestMap,
+ );
}
/// Restore previously purchased transactions.
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 f90684f..cb7ca03 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
@@ -185,4 +185,10 @@
@override
String toString() => _$SKPaymentTransactionWrapperToJson(this).toString();
+
+ /// The payload that is used to finish this transaction.
+ Map<String, String> toFinishMap() => {
+ "transactionIdentifier": this.transactionIdentifier,
+ "productIdentifier": this.payment?.productIdentifier,
+ };
}
diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml
index a9bed88..6dd064e 100644
--- a/packages/in_app_purchase/pubspec.yaml
+++ b/packages/in_app_purchase/pubspec.yaml
@@ -1,7 +1,7 @@
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.4+11
+version: 0.3.4+12
dependencies:
async: ^2.0.8
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 881e1fc..b22737c 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
@@ -92,7 +92,7 @@
test('queryPastPurchases should not block transaction updates', () async {
fakeIOSPlatform.transactions
- .add(fakeIOSPlatform.createPurchasedTransactionWithProductID('foo'));
+ .add(fakeIOSPlatform.createPurchasedTransaction('foo', 'bar'));
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream =
AppStoreConnection.instance.purchaseUpdatedStream;
@@ -348,7 +348,7 @@
testRestoredError = null;
}
- SKPaymentTransactionWrapper createPendingTransactionWithProductID(String id) {
+ SKPaymentTransactionWrapper createPendingTransaction(String id) {
return SKPaymentTransactionWrapper(
transactionIdentifier: null,
payment: SKPaymentWrapper(productIdentifier: id),
@@ -358,21 +358,21 @@
originalTransaction: null);
}
- SKPaymentTransactionWrapper createPurchasedTransactionWithProductID(
- String id) {
+ SKPaymentTransactionWrapper createPurchasedTransaction(
+ String productId, String transactionId) {
return SKPaymentTransactionWrapper(
- payment: SKPaymentWrapper(productIdentifier: id),
+ payment: SKPaymentWrapper(productIdentifier: productId),
transactionState: SKPaymentTransactionStateWrapper.purchased,
transactionTimeStamp: 123123.121,
- transactionIdentifier: id,
+ transactionIdentifier: transactionId,
error: null,
originalTransaction: null);
}
- SKPaymentTransactionWrapper createFailedTransactionWithProductID(String id) {
+ SKPaymentTransactionWrapper createFailedTransaction(String productId) {
return SKPaymentTransactionWrapper(
transactionIdentifier: null,
- payment: SKPaymentWrapper(productIdentifier: id),
+ payment: SKPaymentWrapper(productIdentifier: productId),
transactionState: SKPaymentTransactionStateWrapper.failed,
transactionTimeStamp: 123123.121,
error: SKError(
@@ -434,26 +434,26 @@
return Future<void>.sync(() {});
case '-[InAppPurchasePlugin addPayment:result:]':
String id = call.arguments['productIdentifier'];
- SKPaymentTransactionWrapper transaction =
- createPendingTransactionWithProductID(id);
+ SKPaymentTransactionWrapper transaction = createPendingTransaction(id);
AppStoreConnection.observer
.updatedTransactions(transactions: [transaction]);
sleep(const Duration(milliseconds: 30));
if (testTransactionFail) {
SKPaymentTransactionWrapper transaction_failed =
- createFailedTransactionWithProductID(id);
+ createFailedTransaction(id);
AppStoreConnection.observer
.updatedTransactions(transactions: [transaction_failed]);
} else {
SKPaymentTransactionWrapper transaction_finished =
- createPurchasedTransactionWithProductID(id);
+ createPurchasedTransaction(id, transaction.transactionIdentifier);
AppStoreConnection.observer
.updatedTransactions(transactions: [transaction_finished]);
}
break;
case '-[InAppPurchasePlugin finishTransaction:result:]':
- finishedTransactions
- .add(createPurchasedTransactionWithProductID(call.arguments));
+ finishedTransactions.add(createPurchasedTransaction(
+ call.arguments["productIdentifier"],
+ call.arguments["transactionIdentifier"]));
break;
}
return Future<void>.sync(() {});
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 3a08d9e..92ffbc5 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
@@ -110,7 +110,7 @@
queue.setTransactionObserver(observer);
await queue.finishTransaction(dummyTransaction);
expect(fakeIOSPlatform.transactionsFinished.first,
- equals(dummyTransaction.transactionIdentifier));
+ equals(dummyTransaction.toFinishMap()));
});
test('should restore transaction', () async {
@@ -139,7 +139,7 @@
// payment queue
List<SKPaymentWrapper> payments = [];
- List<String> transactionsFinished = [];
+ List<Map<String, String>> transactionsFinished = [];
String applicationNameHasTransactionRestored;
Future<dynamic> onMethodCall(MethodCall call) {
@@ -171,7 +171,7 @@
payments.add(SKPaymentWrapper.fromJson(call.arguments));
return Future<void>.sync(() {});
case '-[InAppPurchasePlugin finishTransaction:result:]':
- transactionsFinished.add(call.arguments);
+ transactionsFinished.add(Map<String, String>.from(call.arguments));
return Future<void>.sync(() {});
case '-[InAppPurchasePlugin restoreTransactions:result:]':
applicationNameHasTransactionRestored = call.arguments;