From b9ac917daeb0fbf5bd6ad4a7d2a69ab81749d767 Mon Sep 17 00:00:00 2001 From: LouiseHsu Date: Wed, 13 Nov 2024 13:51:05 -0800 Subject: [PATCH] [in_app_purchase_storekit] Fixes manual invocation of `finishTransaction()` triggering fatal crash (#8071) Fixes https://github.com/flutter/flutter/issues/154763 From the Apple docs: https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction `If you call finishTransaction(_:) on a transaction that is in the [SKPaymentTransactionState.purchasing](https://developer.apple.com/documentation/storekit/skpaymenttransactionstate/purchasing) state, StoreKit raises an exception.` For some reason even though the old Obj-C implementation didn't have this check, it didn't crash. This adds an explicit check for the purchasing state. --- .../in_app_purchase_storekit/CHANGELOG.md | 4 ++ .../darwin/Classes/InAppPurchasePlugin.swift | 25 ++++++++---- .../InAppPurchasePluginTests.swift | 40 +++++++++++++++++++ .../in_app_purchase_storekit/pubspec.yaml | 2 +- 4 files changed, 62 insertions(+), 9 deletions(-) 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 5f5456c7f1d..3d4cc8d9837 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 23b8972c96c..8954f2f46b1 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 a074f133161..dd91bc6040a 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 71b78b1d9f0..aa3c1d7b3e6 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