Skip to content

Commit

Permalink
[in_app_purchase_storekit] Fixes manual invocation of `finishTransact…
Browse files Browse the repository at this point in the history
…ion()` triggering fatal crash (flutter#8071)

Fixes flutter/flutter#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.
  • Loading branch information
LouiseHsu authored Nov 13, 2024
1 parent d6f5e1b commit b9ac917
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b9ac917

Please sign in to comment.