[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;