diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index 5f5456c7f1d3..3d4cc8d98378 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.20 + +* Fixes manual invocation of `finishTransaction` causing a fatal crash. + ## 0.3.19+1 * Removes unneeded platform availability annotations. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift index 23b8972c96c1..8954f2f46b13 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift @@ -253,16 +253,25 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, FIAInAppPurchaseAPI { let pendingTransactions = getPaymentQueueHandler().getUnfinishedTransactions() for transaction in pendingTransactions { + // finishTransaction() cannot be called on a Transaction with a current purchasing state + // https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction + guard transaction.transactionState != SKPaymentTransactionState.purchasing else { + continue + } + // 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 == transactionIdentifier - || (transactionIdentifier == nil - && transaction.transactionIdentifier == nil - && transaction.payment.productIdentifier == productIdentifier) - { - getPaymentQueueHandler().finish(transaction) + // So if transactionIdentifier is null AND a transaction in the pendingTransactions list + // also has a null transactionIdentifier, we check for equal product identifiers. + // TODO(louisehsu): See if we can check for SKErrorPaymentCancelled instead. + let matchesTransactionIdentifier = transaction.transactionIdentifier == transactionIdentifier + let isCancelledTransaction = + transactionIdentifier == nil && transaction.transactionIdentifier == nil + && transaction.payment.productIdentifier == productIdentifier + + guard matchesTransactionIdentifier || isCancelledTransaction else { + continue } + getPaymentQueueHandler().finish(transaction) } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift index a074f133161b..dd91bc6040a7 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift @@ -135,6 +135,46 @@ final class InAppPurchasePluginTests: XCTestCase { XCTAssertNil(error) } + func testFinishTransactionNotCalledOnPurchasingTransactions() { + let args: [String: Any] = [ + "transactionIdentifier": NSNull(), + "productIdentifier": "unique_identifier", + ] + + let paymentMap: [String: Any] = [ + "productIdentifier": "123", + "requestData": "abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + "quantity": 2, + "applicationUsername": "app user name", + "simulatesAskToBuyInSandbox": false, + ] + + let transactionMap: [String: Any] = [ + "transactionState": SKPaymentTransactionState.purchasing.rawValue, + "payment": paymentMap, + "error": FIAObjectTranslator.getMapFrom( + NSError(domain: "test_stub", code: 123, userInfo: [:])), + "transactionTimeStamp": NSDate().timeIntervalSince1970, + ] + + let paymentTransactionStub = SKPaymentTransactionStub(map: transactionMap) + + let handler = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handler + + var finishTransactionInvokeCount = 0 + + handler.finishTransactionStub = { _ in + finishTransactionInvokeCount += 1 + } + + var error: FlutterError? + plugin.finishTransactionFinishMap(args, error: &error) + + XCTAssertNil(error) + XCTAssertEqual(finishTransactionInvokeCount, 0) + } + func testGetProductResponseWithRequestError() { let argument = ["123"] let expectation = self.expectation(description: "completion handler successfully called") diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index 71b78b1d9f02..aa3c1d7b3e64 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_storekit description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.19+1 +version: 0.3.20 environment: sdk: ^3.3.0