[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.
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 5f5456c..3d4cc8d 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 23b8972..8954f2f 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 @@
let pendingTransactions = getPaymentQueueHandler().getUnfinishedTransactions()
for transaction in pendingTransactions {
- // 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)
+ // 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 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 a074f13..dd91bc6 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 @@
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 71b78b1..aa3c1d7 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 @@
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