From 43fe14f7eb6590b3f1416a9ea5251d7c4390ea3d Mon Sep 17 00:00:00 2001 From: Ed Pizzi Date: Fri, 19 Dec 2025 13:00:25 -0800 Subject: [PATCH 1/3] Fix an issue where StoreKit 2 purchase transactions remain unfinished. Also resolve an issue where SK2 purchase updates are in state `restoring`. Keep track of when TransactionMessages are created in the restorePurchases code path, instead of incorrectly inferring that transactions with receipts are from the restore code path. This keeps ordinary purcahses as state `purchased`. These purchase detail records will also have `pendingCompletePurchase` set to `true`, since this is gated by state `purchased`. --- .../StoreKit2/InAppPurchasePlugin+StoreKit2.swift | 9 ++++++--- .../StoreKit2/StoreKit2Translators.swift | 4 ++-- .../test/in_app_purchase_storekit_2_platform_test.dart | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift index f6d72996517..d7a1cd9fba0 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift @@ -261,7 +261,8 @@ extension InAppPurchasePlugin: InAppPurchase2API { switch completedPurchase { case .verified(let purchase): self.sendTransactionUpdate( - transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)") + transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)", + restoring: true) case .unverified(let failedPurchase, let error): unverifiedPurchases[failedPurchase.id] = ( receipt: completedPurchase.jwsRepresentation, error: error @@ -354,8 +355,10 @@ extension InAppPurchasePlugin: InAppPurchase2API { } /// Sends an transaction back to Dart. Access these transactions with `purchaseStream` - private func sendTransactionUpdate(transaction: Transaction, receipt: String? = nil) { - let transactionMessage = transaction.convertToPigeon(receipt: receipt) + private func sendTransactionUpdate( + transaction: Transaction, receipt: String? = nil, restoring: Bool = false + ) { + let transactionMessage = transaction.convertToPigeon(receipt: receipt, restoring: restoring) Task { @MainActor in self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: [transactionMessage]) { result in diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Translators.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Translators.swift index 19f18688e20..d71af427414 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Translators.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Translators.swift @@ -192,7 +192,7 @@ extension Product.PurchaseResult { @available(iOS 15.0, macOS 12.0, *) extension Transaction { - func convertToPigeon(receipt: String?) -> SK2TransactionMessage { + func convertToPigeon(receipt: String?, restoring: Bool = false) -> SK2TransactionMessage { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" @@ -205,7 +205,7 @@ extension Transaction { expirationDate: expirationDate.map { dateFormatter.string(from: $0) }, purchasedQuantity: Int64(purchasedQuantity), appAccountToken: appAccountToken?.uuidString, - restoring: receipt != nil, + restoring: restoring, receiptData: receipt, jsonRepresentation: String(decoding: jsonRepresentation, as: UTF8.self) ) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart index 35191b98a20..cc95c167b80 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart @@ -120,6 +120,8 @@ void main() { final List result = await completer.future; expect(result.length, 1); expect(result.first.productID, dummyProductWrapper.id); + expect(result.first.status, PurchaseStatus.purchased); + expect(result.first.pendingCompletePurchase, true); }, ); From 04f95f793db348a08364d080c1fd18025e231c78 Mon Sep 17 00:00:00 2001 From: Ed Pizzi Date: Sat, 20 Dec 2025 09:17:29 -0800 Subject: [PATCH 2/3] changelog and pubspec version --- .../in_app_purchase/in_app_purchase_storekit/CHANGELOG.md | 4 +++- .../in_app_purchase/in_app_purchase_storekit/pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 2 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 b3c9557f21b..3f220f595ac 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 0.4.8 +* Fixes an issue causing StoreKit2 purchases to be reported as `restored` and left in an + unfinished state, due to `pendingCompletePurchase` being false. * Fixes Xcode 26.2 analyzer warnings in example app tests. ## 0.4.7 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 5882c914518..44774ace5b8 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.4.7 +version: 0.4.8 environment: sdk: ^3.9.0 From 626cb28a389c2233c0e6d1f06dbef4eec8d47cfc Mon Sep 17 00:00:00 2001 From: Ed Pizzi Date: Sat, 20 Dec 2025 21:57:09 -0800 Subject: [PATCH 3/3] Test restoring value for the purchase, restoring, and transaction codepaths. --- .../InAppPurchaseStoreKit2PluginTests.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift index 2a34c959bc8..095471d8b8e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift @@ -8,10 +8,14 @@ import XCTest @testable import in_app_purchase_storekit final class FakeIAP2Callback: InAppPurchase2CallbackAPIProtocol { + + public var lastUpdate: [in_app_purchase_storekit.SK2TransactionMessage] = [] + func onTransactionsUpdated( newTransactions newTransactionsArg: [in_app_purchase_storekit.SK2TransactionMessage], completion: @escaping (Result) -> Void ) { + lastUpdate = newTransactionsArg // We should only write to a flutter channel from the main thread. XCTAssertTrue(Thread.isMainThread) } @@ -21,6 +25,7 @@ final class FakeIAP2Callback: InAppPurchase2CallbackAPIProtocol { final class InAppPurchase2PluginTests: XCTestCase { private var session: SKTestSession! private var plugin: InAppPurchasePlugin! + private var callback: FakeIAP2Callback = FakeIAP2Callback() override func setUp() async throws { try await super.setUp() @@ -33,7 +38,7 @@ final class InAppPurchase2PluginTests: XCTestCase { plugin = InAppPurchasePluginStub(receiptManager: FIAPReceiptManagerStub()) { request in DefaultRequestHandler(requestHandler: FIAPRequestHandler(request: request)) } - plugin.transactionCallbackAPI = FakeIAP2Callback() + plugin.transactionCallbackAPI = callback try plugin.startListeningToTransactions() } @@ -86,11 +91,17 @@ final class InAppPurchase2PluginTests: XCTestCase { await fulfillment(of: [purchaseExpectation], timeout: 5) + XCTAssert(callback.lastUpdate.count == 1) + XCTAssert( + callback.lastUpdate.first?.restoring == false, + "Ordinary purchase updates should not be marked as restoring") + plugin.transactions { result in switch result { case .success(let transactions): XCTAssert(transactions.count == 1) + XCTAssert(transactions.first?.restoring == false) transactionExpectation.fulfill() case .failure(let error): XCTFail("Getting transactions should NOT fail. Failed with \(error)") @@ -376,6 +387,9 @@ final class InAppPurchase2PluginTests: XCTestCase { } await fulfillment(of: [purchaseExpectation], timeout: 5) + XCTAssert(callback.lastUpdate.count == 1) + XCTAssert(callback.lastUpdate.first?.restoring == false) + plugin.restorePurchases { result in switch result { case .success(): @@ -385,6 +399,9 @@ final class InAppPurchase2PluginTests: XCTestCase { } } await fulfillment(of: [restoreExpectation], timeout: 5) + + XCTAssert(callback.lastUpdate.count == 1) + XCTAssert(callback.lastUpdate.first?.restoring == true) } func testFinishTransaction() async throws {