diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index f1b728b5387a..a436a5e29a25 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 6bcd58e73b96..872a34a94954 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -199,19 +199,27 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { } - (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 *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 7f20736afbf8..bb731ac3f2aa 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 @@ class SKPaymentQueueWrapper { /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). Future finishTransaction( SKPaymentTransactionWrapper transaction) async { + Map requestMap = transaction.toFinishMap(); await channel.invokeMethod( - '-[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 f90684f374f5..cb7ca03f38bc 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 @@ class SKPaymentTransactionWrapper { @override String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); + + /// The payload that is used to finish this transaction. + Map 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 a9bed88f1f65..6dd064e8af57 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 881e1fcc75b7..b22737ca041b 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 @@ void main() { test('queryPastPurchases should not block transaction updates', () async { fakeIOSPlatform.transactions - .add(fakeIOSPlatform.createPurchasedTransactionWithProductID('foo')); + .add(fakeIOSPlatform.createPurchasedTransaction('foo', 'bar')); Completer completer = Completer(); Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; @@ -348,7 +348,7 @@ class FakeIOSPlatform { testRestoredError = null; } - SKPaymentTransactionWrapper createPendingTransactionWithProductID(String id) { + SKPaymentTransactionWrapper createPendingTransaction(String id) { return SKPaymentTransactionWrapper( transactionIdentifier: null, payment: SKPaymentWrapper(productIdentifier: id), @@ -358,21 +358,21 @@ class FakeIOSPlatform { 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 @@ class FakeIOSPlatform { return Future.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.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 3a08d9e8e45d..92ffbc5797e3 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 @@ void main() { 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 @@ class FakeIOSPlatform { // payment queue List payments = []; - List transactionsFinished = []; + List> transactionsFinished = []; String applicationNameHasTransactionRestored; Future onMethodCall(MethodCall call) { @@ -171,7 +171,7 @@ class FakeIOSPlatform { payments.add(SKPaymentWrapper.fromJson(call.arguments)); return Future.sync(() {}); case '-[InAppPurchasePlugin finishTransaction:result:]': - transactionsFinished.add(call.arguments); + transactionsFinished.add(Map.from(call.arguments)); return Future.sync(() {}); case '-[InAppPurchasePlugin restoreTransactions:result:]': applicationNameHasTransactionRestored = call.arguments;