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..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,5 +1,10 @@ ## NEXT +* 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 Xcode 26.2 analyzer warnings in example app tests. ## 0.4.7 @@ -18,7 +23,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 @@ -47,7 +52,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 f6d729965174..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)") + transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)", + isRestoring: 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, 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/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..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 @@ -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 details = SK2PurchaseDetails( + productID: purchaseParam.productDetails.id, + purchaseID: null, + verificationData: 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 5882c9145184..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 +version: 0.4.7+2 environment: sdk: ^3.9.0