Skip to content

Commit 0fd5582

Browse files
edpizziandroidseb
authored andcommitted
[in_app_purchase] Write to the transactions update queue from the main thread (flutter#9068)
Equeue to the channel from the main thread to avoid channel safety issues. Background: `sendTransactionUpdate` can be called from the main thread, where it is safe to send messages on the `onTransactionsUpdated` channel. This seems to happen when the purchase code path succeeds and generates a transaction. However `sendTransactionUpdate` can also be called on non-main threads serving background executors, when transaction callbacks result from restoring purchases or other transactions are reported to the transaction subscription (eg. a purchase made outside of the app, or a refund is issued -- this can be triggered from XCode's StoreKit Test transaction log). In these cases, at least sometimes, an error is logged about writing to a channel on a non-platform thread (see the issue below). Enqueue on the main thread to avoid channel thread-safety issues. This could be optimized further, but I believe this is safe. Resolves flutter/flutter#166493 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent e96d7d8 commit 0fd5582

File tree

5 files changed

+28
-11
lines changed

5 files changed

+28
-11
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.22+1
2+
3+
* Fix a channel thread-safety issue when StoreKit2 is enabled.
4+
15
## 0.3.22
26

37
* Adds `sync()` and `countryCode()`.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, FIAInAppPurchaseAPI {
4444
self._updateListenerTask = task
4545
}
4646

47-
var transactionCallbackAPI: InAppPurchase2CallbackAPI? = nil
47+
var transactionCallbackAPI: InAppPurchase2CallbackAPIProtocol? = nil
4848

4949
public static func register(with registrar: FlutterPluginRegistrar) {
5050
#if os(iOS)

packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,14 @@ extension InAppPurchasePlugin: InAppPurchase2API {
207207
/// Sends an transaction back to Dart. Access these transactions with `purchaseStream`
208208
private func sendTransactionUpdate(transaction: Transaction, receipt: String? = nil) {
209209
let transactionMessage = transaction.convertToPigeon(receipt: receipt)
210-
self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: [transactionMessage]) {
211-
result in
212-
switch result {
213-
case .success: break
214-
case .failure(let error):
215-
print("Failed to send transaction updates: \(error)")
210+
Task { @MainActor in
211+
self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: [transactionMessage]) {
212+
result in
213+
switch result {
214+
case .success: break
215+
case .failure(let error):
216+
print("Failed to send transaction updates: \(error)")
217+
}
216218
}
217219
}
218220
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ import XCTest
77

88
@testable import in_app_purchase_storekit
99

10-
@available(iOS 15.0, macOS 12.0, *)
10+
final class FakeIAP2Callback: InAppPurchase2CallbackAPIProtocol {
11+
func onTransactionsUpdated(
12+
newTransactions newTransactionsArg: [in_app_purchase_storekit.SK2TransactionMessage],
13+
completion: @escaping (Result<Void, in_app_purchase_storekit.PigeonError>) -> Void
14+
) {
15+
// We should only write to a flutter channel from the main thread.
16+
XCTAssertTrue(Thread.isMainThread)
17+
}
18+
}
1119

20+
@available(iOS 15.0, macOS 12.0, *)
1221
final class InAppPurchase2PluginTests: XCTestCase {
1322
private var session: SKTestSession!
1423
private var plugin: InAppPurchasePlugin!
@@ -24,6 +33,7 @@ final class InAppPurchase2PluginTests: XCTestCase {
2433
plugin = InAppPurchasePluginStub(receiptManager: FIAPReceiptManagerStub()) { request in
2534
DefaultRequestHandler(requestHandler: FIAPRequestHandler(request: request))
2635
}
36+
plugin.transactionCallbackAPI = FakeIAP2Callback()
2737
try plugin.startListeningToTransactions()
2838
}
2939

@@ -296,6 +306,8 @@ final class InAppPurchase2PluginTests: XCTestCase {
296306
XCTFail("Purchase should NOT fail. Failed with \(error)")
297307
}
298308
}
309+
await fulfillment(of: [purchaseExpectation], timeout: 5)
310+
299311
plugin.restorePurchases { result in
300312
switch result {
301313
case .success():
@@ -304,8 +316,7 @@ final class InAppPurchase2PluginTests: XCTestCase {
304316
XCTFail("Restore purchases should NOT fail. Failed with \(error)")
305317
}
306318
}
307-
308-
await fulfillment(of: [restoreExpectation, purchaseExpectation], timeout: 5)
319+
await fulfillment(of: [restoreExpectation], timeout: 5)
309320
}
310321

311322
func testFinishTransaction() async throws {

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.22
5+
version: 0.3.22+1
66

77
environment:
88
sdk: ^3.4.0

0 commit comments

Comments
 (0)