From 94e48cbeca18b53c380937c98e77e09263473c04 Mon Sep 17 00:00:00 2001 From: arthur Date: Wed, 24 Dec 2025 00:06:08 +0800 Subject: [PATCH 1/6] [in_app_purchase_storekit] Fixes StoreKit 2 consumable purchases being reported as restored, which left transactions --- .../in_app_purchase/in_app_purchase_storekit/CHANGELOG.md | 3 +++ .../StoreKit2/InAppPurchasePlugin+StoreKit2.swift | 6 +++--- .../StoreKit2/StoreKit2Translators.swift | 4 ++-- .../in_app_purchase/in_app_purchase_storekit/pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 6 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 b3c9557f21b5..9407ef5a5529 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,8 @@ ## NEXT +## 0.4.7+1 + +* Fixes StoreKit 2 consumable purchases being reported as restored, which left transactions unfinished and blocked repeat buys. * Fixes Xcode 26.2 analyzer warnings in example app tests. ## 0.4.7 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 f6d729965174..6f9fb68596dc 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,7 @@ extension InAppPurchasePlugin: InAppPurchase2API { switch completedPurchase { case .verified(let purchase): self.sendTransactionUpdate( - transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)") + transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)", isRestoring: true) case .unverified(let failedPurchase, let error): unverifiedPurchases[failedPurchase.id] = ( receipt: completedPurchase.jwsRepresentation, error: error @@ -354,8 +354,8 @@ 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, isRestoring: Bool = false) { + let transactionMessage = transaction.convertToPigeon(receipt: receipt, isRestoring: isRestoring) 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 19f18688e208..387512eb442b 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?, isRestoring: 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: isRestoring, receiptData: receipt, jsonRepresentation: String(decoding: jsonRepresentation, as: UTF8.self) ) 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 5882c9145184..b52c552a8207 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.7+1 environment: sdk: ^3.9.0 From 7937bc837c593ebca2dfe7e48c595cb0d032ea08 Mon Sep 17 00:00:00 2001 From: arthur Date: Wed, 24 Dec 2025 00:10:02 +0800 Subject: [PATCH 2/6] [in_app_purchase_storekit] Fixes StoreKit 2 purchase flow to send cancelled/pending/unverified results to purchaseStream. --- .../in_app_purchase_storekit/CHANGELOG.md | 4 ++ .../in_app_purchase_storekit_platform.dart | 38 ++++++++++++++++++- .../in_app_purchase_storekit/pubspec.yaml | 2 +- 3 files changed, 42 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 9407ef5a5529..5500155c2a57 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,9 @@ ## NEXT +## 0.4.7+2 + +* Fixes StoreKit 2 purchase flow to send cancelled/pending/unverified results to `purchaseStream`. + ## 0.4.7+1 * Fixes StoreKit 2 consumable purchases being reported as restored, which left transactions unfinished and blocked repeat buys. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 05c0262027e3..126b18f9e62a 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -178,11 +178,47 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { ); } - await SK2Product.purchase( + final SK2ProductPurchaseResult result = await SK2Product.purchase( purchaseParam.productDetails.id, options: options, ); + // For non-success results, manually send update to the stream + // since native side only sends transaction for success case + if (result != SK2ProductPurchaseResult.success) { + + final PurchaseStatus status = switch (result) { + SK2ProductPurchaseResult.userCancelled => PurchaseStatus.canceled, + SK2ProductPurchaseResult.pending => PurchaseStatus.pending, + SK2ProductPurchaseResult.unverified => PurchaseStatus.error, + SK2ProductPurchaseResult.success => + PurchaseStatus.purchased, // won't reach here + }; + + final SK2PurchaseDetails details = SK2PurchaseDetails( + productID: purchaseParam.productDetails.id, + purchaseID: null, + verificationData: const PurchaseVerificationData( + localVerificationData: '', + serverVerificationData: '', + source: kIAPSource, + ), + transactionDate: null, + status: status, + ); + + if (status == PurchaseStatus.error) { + details.error = IAPError( + source: kIAPSource, + code: kPurchaseErrorCode, + message: 'Purchase verification failed', + ); + } + + _sk2transactionObserver.transactionsCreatedController + .add([details]); + } + return true; } await _skPaymentQueueWrapper.addPayment( 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 b52c552a8207..bce400b5d748 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+1 +version: 0.4.7+2 environment: sdk: ^3.9.0 From 26ba86e94828882688f1c549714e639dadd15984 Mon Sep 17 00:00:00 2001 From: arthur Date: Wed, 24 Dec 2025 10:02:41 +0800 Subject: [PATCH 3/6] [in_app_purchase_storekit] Remove const from PurchaseVerificationData initialization --- .../lib/src/in_app_purchase_storekit_platform.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 126b18f9e62a..28c8433984bf 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -198,7 +198,7 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { final SK2PurchaseDetails details = SK2PurchaseDetails( productID: purchaseParam.productDetails.id, purchaseID: null, - verificationData: const PurchaseVerificationData( + verificationData: PurchaseVerificationData( localVerificationData: '', serverVerificationData: '', source: kIAPSource, From 5f0c5930d9561fa66b55123d947071c2b99138bc Mon Sep 17 00:00:00 2001 From: arthur Date: Wed, 24 Dec 2025 14:36:52 +0800 Subject: [PATCH 4/6] [in_app_purchase_storekit] Update CHANGELOG.md; optimize code formatting --- .../in_app_purchase/in_app_purchase_storekit/CHANGELOG.md | 6 +++--- .../StoreKit2/InAppPurchasePlugin+StoreKit2.swift | 7 +++++-- .../lib/src/in_app_purchase_storekit_platform.dart | 6 +++--- 3 files changed, 11 insertions(+), 8 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 5500155c2a57..adeab643cc54 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -3,10 +3,10 @@ ## 0.4.7+2 * Fixes StoreKit 2 purchase flow to send cancelled/pending/unverified results to `purchaseStream`. +* Fixes StoreKit 2 consumable purchases being reported as restored, which left transactions unfinished and blocked repeat buys. ## 0.4.7+1 -* Fixes StoreKit 2 consumable purchases being reported as restored, which left transactions unfinished and blocked repeat buys. * Fixes Xcode 26.2 analyzer warnings in example app tests. ## 0.4.7 @@ -25,7 +25,7 @@ ## 0.4.6 * Adds a new case `.unverified` to enum `SK2ProductPurchaseResult` -* Fixes the StoreKit2 implementation throwing `PlatformException`s instead of returning the corresponding +* Fixes the StoreKit2 implementation throwing `PlatformException`s instead of returning the corresponding `SK2ProductPurchaseResult` when a purchase is cancelled / unverified / pending. ## 0.4.5 @@ -54,7 +54,7 @@ * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. * Adds **Win Back Offers** support for StoreKit2: - - Includes new `isWinBackOfferEligible` function for eligibility verification + * Includes new `isWinBackOfferEligible` function for eligibility verification * Adds **Promotional Offers** support in StoreKit2 purchases * Fixes introductory pricing handling in promotional offers list in StoreKit2 * Ensures proper `appAccountToken` handling for StoreKit2 purchases 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 6f9fb68596dc..6f9d92ec4a97 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)", isRestoring: true) + transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)", + isRestoring: true) case .unverified(let failedPurchase, let error): unverifiedPurchases[failedPurchase.id] = ( receipt: completedPurchase.jwsRepresentation, error: error @@ -354,7 +355,9 @@ extension InAppPurchasePlugin: InAppPurchase2API { } /// Sends an transaction back to Dart. Access these transactions with `purchaseStream` - private func sendTransactionUpdate(transaction: Transaction, receipt: String? = nil, isRestoring: Bool = false) { + private func sendTransactionUpdate( + transaction: Transaction, receipt: String? = nil, isRestoring: Bool = false + ) { let transactionMessage = transaction.convertToPigeon(receipt: receipt, isRestoring: isRestoring) Task { @MainActor in self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: [transactionMessage]) { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 28c8433984bf..9a29ce91da3a 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -186,7 +186,6 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { // For non-success results, manually send update to the stream // since native side only sends transaction for success case if (result != SK2ProductPurchaseResult.success) { - final PurchaseStatus status = switch (result) { SK2ProductPurchaseResult.userCancelled => PurchaseStatus.canceled, SK2ProductPurchaseResult.pending => PurchaseStatus.pending, @@ -215,8 +214,9 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { ); } - _sk2transactionObserver.transactionsCreatedController - .add([details]); + _sk2transactionObserver.transactionsCreatedController.add( + [details], + ); } return true; From 4a053c40d9b0855a318e0cb1096650db9969980a Mon Sep 17 00:00:00 2001 From: arthur Date: Wed, 24 Dec 2025 18:42:59 +0800 Subject: [PATCH 5/6] [in_app_purchase_storekit] resolve analyzer error --- .../lib/src/in_app_purchase_storekit_platform.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 9a29ce91da3a..7ebdd717353f 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -194,7 +194,7 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { PurchaseStatus.purchased, // won't reach here }; - final SK2PurchaseDetails details = SK2PurchaseDetails( + final details = SK2PurchaseDetails( productID: purchaseParam.productDetails.id, purchaseID: null, verificationData: PurchaseVerificationData( From fe6a33e0dd0590ef6b6a76bd24fd981ea0f5fe5a Mon Sep 17 00:00:00 2001 From: arthur Date: Wed, 24 Dec 2025 19:01:27 +0800 Subject: [PATCH 6/6] [in_app_purchase_storekit] fix empty NEXT section in CHANGELOG --- packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md | 2 -- 1 file changed, 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 adeab643cc54..23c1ff74f948 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,7 +1,5 @@ ## NEXT -## 0.4.7+2 - * Fixes StoreKit 2 purchase flow to send cancelled/pending/unverified results to `purchaseStream`. * Fixes StoreKit 2 consumable purchases being reported as restored, which left transactions unfinished and blocked repeat buys.