Skip to content

Commit b9ac917

Browse files
authored
[in_app_purchase_storekit] Fixes manual invocation of finishTransaction() triggering fatal crash (#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.
1 parent d6f5e1b commit b9ac917

File tree

4 files changed

+62
-9
lines changed

4 files changed

+62
-9
lines changed

packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.3.20
2+
3+
* Fixes manual invocation of `finishTransaction` causing a fatal crash.
4+
15
## 0.3.19+1
26

37
* Removes unneeded platform availability annotations.

packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,25 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, FIAInAppPurchaseAPI {
253253
let pendingTransactions = getPaymentQueueHandler().getUnfinishedTransactions()
254254

255255
for transaction in pendingTransactions {
256+
// finishTransaction() cannot be called on a Transaction with a current purchasing state
257+
// https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction
258+
guard transaction.transactionState != SKPaymentTransactionState.purchasing else {
259+
continue
260+
}
261+
256262
// If the user cancels the purchase dialog we won't have a transactionIdentifier.
257-
// So if it is null AND a transaction in the pendingTransactions list has
258-
// also a null transactionIdentifier we check for equal product identifiers.
259-
if transaction.transactionIdentifier == transactionIdentifier
260-
|| (transactionIdentifier == nil
261-
&& transaction.transactionIdentifier == nil
262-
&& transaction.payment.productIdentifier == productIdentifier)
263-
{
264-
getPaymentQueueHandler().finish(transaction)
263+
// So if transactionIdentifier is null AND a transaction in the pendingTransactions list
264+
// also has a null transactionIdentifier, we check for equal product identifiers.
265+
// TODO(louisehsu): See if we can check for SKErrorPaymentCancelled instead.
266+
let matchesTransactionIdentifier = transaction.transactionIdentifier == transactionIdentifier
267+
let isCancelledTransaction =
268+
transactionIdentifier == nil && transaction.transactionIdentifier == nil
269+
&& transaction.payment.productIdentifier == productIdentifier
270+
271+
guard matchesTransactionIdentifier || isCancelledTransaction else {
272+
continue
265273
}
274+
getPaymentQueueHandler().finish(transaction)
266275
}
267276
}
268277

packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,46 @@ final class InAppPurchasePluginTests: XCTestCase {
135135
XCTAssertNil(error)
136136
}
137137

138+
func testFinishTransactionNotCalledOnPurchasingTransactions() {
139+
let args: [String: Any] = [
140+
"transactionIdentifier": NSNull(),
141+
"productIdentifier": "unique_identifier",
142+
]
143+
144+
let paymentMap: [String: Any] = [
145+
"productIdentifier": "123",
146+
"requestData": "abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh",
147+
"quantity": 2,
148+
"applicationUsername": "app user name",
149+
"simulatesAskToBuyInSandbox": false,
150+
]
151+
152+
let transactionMap: [String: Any] = [
153+
"transactionState": SKPaymentTransactionState.purchasing.rawValue,
154+
"payment": paymentMap,
155+
"error": FIAObjectTranslator.getMapFrom(
156+
NSError(domain: "test_stub", code: 123, userInfo: [:])),
157+
"transactionTimeStamp": NSDate().timeIntervalSince1970,
158+
]
159+
160+
let paymentTransactionStub = SKPaymentTransactionStub(map: transactionMap)
161+
162+
let handler = PaymentQueueHandlerStub()
163+
plugin.paymentQueueHandler = handler
164+
165+
var finishTransactionInvokeCount = 0
166+
167+
handler.finishTransactionStub = { _ in
168+
finishTransactionInvokeCount += 1
169+
}
170+
171+
var error: FlutterError?
172+
plugin.finishTransactionFinishMap(args, error: &error)
173+
174+
XCTAssertNil(error)
175+
XCTAssertEqual(finishTransactionInvokeCount, 0)
176+
}
177+
138178
func testGetProductResponseWithRequestError() {
139179
let argument = ["123"]
140180
let expectation = self.expectation(description: "completion handler successfully called")

packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: in_app_purchase_storekit
22
description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
33
repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
5-
version: 0.3.19+1
5+
version: 0.3.20
66

77
environment:
88
sdk: ^3.3.0

0 commit comments

Comments
 (0)