From d0a18ee7b3015ca68c9706233c3060c80b992213 Mon Sep 17 00:00:00 2001 From: Absar Date: Fri, 15 May 2026 12:20:03 +0200 Subject: [PATCH 1/2] [in_app_purchase] Fixes StoreKit 2 date format does not match in_app_purchase_platform_interface - As per `PurchaseDetails.transactionDate` api documentation, date should be `Milliseconds since epoch` but the migration to StoreKit2 probably mistakenly changed the date format to "yyyy-MM-dd HH:mm:ss". The issue originated in StoreKit2Translators.swift `extension Transaction convertToPigeon` which was using a magic hardcoded formatter dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - Fixes both `SK2Transaction.purchaseDate` and `SK2Transaction.expirationDate` and ultimately `PurchaseDetails.transactionDate` Fixes https://github.com/flutter/flutter/issues/175072 --- .../in_app_purchase_storekit/CHANGELOG.md | 5 +++++ .../StoreKit2/StoreKit2Messages.g.swift | 8 ++++---- .../StoreKit2/StoreKit2Translators.swift | 8 ++------ .../lib/src/sk2_pigeon.g.dart | 8 ++++---- .../sk2_transaction_wrapper.dart | 19 ++++++++++++++++--- .../pigeons/sk2_pigeon.dart | 4 ++-- .../in_app_purchase_storekit/pubspec.yaml | 2 +- .../test/fakes/fake_storekit_platform.dart | 11 ++++++----- ...app_purchase_storekit_2_platform_test.dart | 12 ++++++++++++ 9 files changed, 52 insertions(+), 25 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 20ae28ac9cfc..6fe9e3ced328 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.4.11 + +* Fixes StoreKit 2 date format does not match in_app_purchase_platform_interface PurchaseDetails.transactionDate format. + Fixes both `SK2Transaction.purchaseDate` and `SK2Transaction.expirationDate`. + ## 0.4.10 * Clarifies `completePurchase` usage and the consequences of unfinished transactions in the README and API docstrings. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift index dc75c7dae891..faeae556d8fe 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift @@ -507,8 +507,8 @@ struct SK2TransactionMessage: Hashable { var id: Int64 var originalId: Int64 var productId: String - var purchaseDate: String? = nil - var expirationDate: String? = nil + var purchaseDate: Double? = nil + var expirationDate: Double? = nil var purchasedQuantity: Int64 var appAccountToken: String? = nil var receiptData: String? = nil @@ -523,8 +523,8 @@ struct SK2TransactionMessage: Hashable { let id = pigeonVar_list[0] as! Int64 let originalId = pigeonVar_list[1] as! Int64 let productId = pigeonVar_list[2] as! String - let purchaseDate: String? = nilOrValue(pigeonVar_list[3]) - let expirationDate: String? = nilOrValue(pigeonVar_list[4]) + let purchaseDate: Double? = nilOrValue(pigeonVar_list[3]) + let expirationDate: Double? = nilOrValue(pigeonVar_list[4]) let purchasedQuantity = pigeonVar_list[5] as! Int64 let appAccountToken: String? = nilOrValue(pigeonVar_list[6]) let receiptData: String? = nilOrValue(pigeonVar_list[7]) 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 ae2cf5530d50..70bdad7b6b0f 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 @@ -194,16 +194,12 @@ extension Product.PurchaseResult { extension Transaction { func convertToPigeon(receipt: String?, status: SK2PurchaseStatusMessage) -> SK2TransactionMessage { - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - return SK2TransactionMessage( id: Int64(id), originalId: Int64(originalID), productId: productID, - purchaseDate: dateFormatter.string(from: purchaseDate), - expirationDate: expirationDate.map { dateFormatter.string(from: $0) }, + purchaseDate: purchaseDate.timeIntervalSince1970, + expirationDate: expirationDate.map { $0.timeIntervalSince1970 }, purchasedQuantity: Int64(purchasedQuantity), appAccountToken: appAccountToken?.uuidString, receiptData: receipt, diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart index cbbdbb5aef80..8d1423449abb 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart @@ -545,9 +545,9 @@ class SK2TransactionMessage { String productId; - String? purchaseDate; + double? purchaseDate; - String? expirationDate; + double? expirationDate; int purchasedQuantity; @@ -589,8 +589,8 @@ class SK2TransactionMessage { id: result[0]! as int, originalId: result[1]! as int, productId: result[2]! as String, - purchaseDate: result[3] as String?, - expirationDate: result[4] as String?, + purchaseDate: result[3] as double?, + expirationDate: result[4] as double?, purchasedQuantity: result[5]! as int, appAccountToken: result[6] as String?, receiptData: result[7] as String?, diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart index 660242745942..aa3ba8a691b2 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart @@ -44,9 +44,13 @@ class SK2Transaction { /// The date that the App Store charged the user's account for a purchased or /// restored product, or for a subscription purchase or renewal after a lapse. + /// + /// Milliseconds since epoch. final String purchaseDate; /// The date the subscription expires or renews. + /// + /// Milliseconds since epoch. final String? expirationDate; /// The number of consumable products purchased. @@ -124,8 +128,12 @@ extension on SK2TransactionMessage { id: id.toString(), originalId: originalId.toString(), productId: productId, - purchaseDate: purchaseDate ?? '', - expirationDate: expirationDate, + purchaseDate: purchaseDate != null + ? _secondsToMillisecondsSinceEpochString(purchaseDate!) + : '', + expirationDate: expirationDate != null + ? _secondsToMillisecondsSinceEpochString(expirationDate!) + : null, appAccountToken: appAccountToken, receiptData: receiptData, jsonRepresentation: jsonRepresentation, @@ -153,12 +161,17 @@ extension on SK2TransactionMessage { serverVerificationData: receiptData ?? '', source: kIAPSource, ), - transactionDate: purchaseDate, + transactionDate: purchaseDate != null + ? _secondsToMillisecondsSinceEpochString(purchaseDate!) + : null, status: purchaseStatus, purchaseID: id > 0 ? id.toString() : null, appAccountToken: appAccountToken, ); } + + String _secondsToMillisecondsSinceEpochString(double date) => + (date * 1000).round().toString(); } /// An observer that listens to all transactions created diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart index fca52d2a402e..1d0442412b66 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart @@ -179,8 +179,8 @@ class SK2TransactionMessage { final int id; final int originalId; final String productId; - final String? purchaseDate; - final String? expirationDate; + final double? purchaseDate; + final double? expirationDate; final int purchasedQuantity; final String? appAccountToken; final String? receiptData; 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 4a3746860f68..441ec10e2f05 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.10 +version: 0.4.11 environment: sdk: ^3.10.0 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart index 33af9503f736..76c16a5ac68b 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -359,7 +359,6 @@ class FakeStoreKit2Platform implements InAppPurchase2API { id: 123, originalId: 321, productId: '', - purchaseDate: '', appAccountToken: '', status: SK2PurchaseStatusMessage.restored, ); @@ -406,7 +405,8 @@ class FakeStoreKit2Platform implements InAppPurchase2API { id: 1, originalId: 2, productId: id, - purchaseDate: 'purchaseDate', + purchaseDate: 123123.121, + expirationDate: 321321.32, appAccountToken: 'appAccountToken', receiptData: 'receiptData', jsonRepresentation: 'jsonRepresentation', @@ -454,7 +454,7 @@ class FakeStoreKit2Platform implements InAppPurchase2API { id: 123, originalId: 123, productId: 'product_id', - purchaseDate: '12-12', + purchaseDate: 123123.121, status: SK2PurchaseStatusMessage.purchased, ), ]); @@ -467,7 +467,8 @@ class FakeStoreKit2Platform implements InAppPurchase2API { id: 123, originalId: 123, productId: 'product_id', - purchaseDate: '12-12', + purchaseDate: 123123.121, + expirationDate: 321321.32, receiptData: 'fake_jws_representation', appAccountToken: 'fake_app_account_token', status: SK2PurchaseStatusMessage.purchased, @@ -557,7 +558,7 @@ SK2TransactionMessage createPendingTransaction(String id, {int quantity = 1}) { id: 1, originalId: 2, productId: id, - purchaseDate: 'purchaseDate', + purchaseDate: 123123.121, appAccountToken: 'appAccountToken', receiptData: 'receiptData', jsonRepresentation: 'jsonRepresentation', 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 883a6c468698..b3b89d6b3b71 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 @@ -114,6 +114,10 @@ void main() { expect(result.first.productID, dummyProductWrapper.id); expect(result.first.status, PurchaseStatus.purchased); expect(result.first.pendingCompletePurchase, true); + expect( + result.first.transactionDate, + (123123.121 * 1000).round().toString(), + ); }, ); @@ -141,6 +145,10 @@ void main() { final List result = await completer.future; expect(result.length, 1); expect(result.first.productID, dummyProductWrapper.id); + expect( + result.first.transactionDate, + (123123.121 * 1000).round().toString(), + ); }, ); @@ -638,6 +646,10 @@ void main() { expect(transactions, isNotEmpty); expect(transactions.first.id, '123'); expect(transactions.first.productId, 'product_id'); + expect( + transactions.first.expirationDate, + (321321.32 * 1000).round().toString(), + ); }); test('should expose receiptData (JWS) in unfinished transactions', () async { From efc7bc1b6fdec053cc083e1a747a93ecc6861780 Mon Sep 17 00:00:00 2001 From: Absar Date: Wed, 17 Jun 2026 12:24:56 +0200 Subject: [PATCH 2/2] Reformat --- .../sk2_transaction_wrapper.dart | 3 +-- .../in_app_purchase_storekit_2_platform_test.dart | 15 +++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart index aa3ba8a691b2..e4625aae9fe2 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart @@ -170,8 +170,7 @@ extension on SK2TransactionMessage { ); } - String _secondsToMillisecondsSinceEpochString(double date) => - (date * 1000).round().toString(); + String _secondsToMillisecondsSinceEpochString(double date) => (date * 1000).round().toString(); } /// An observer that listens to all transactions created 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 b3b89d6b3b71..77bd204a7af5 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 @@ -114,10 +114,7 @@ void main() { expect(result.first.productID, dummyProductWrapper.id); expect(result.first.status, PurchaseStatus.purchased); expect(result.first.pendingCompletePurchase, true); - expect( - result.first.transactionDate, - (123123.121 * 1000).round().toString(), - ); + expect(result.first.transactionDate, (123123.121 * 1000).round().toString()); }, ); @@ -145,10 +142,7 @@ void main() { final List result = await completer.future; expect(result.length, 1); expect(result.first.productID, dummyProductWrapper.id); - expect( - result.first.transactionDate, - (123123.121 * 1000).round().toString(), - ); + expect(result.first.transactionDate, (123123.121 * 1000).round().toString()); }, ); @@ -646,10 +640,7 @@ void main() { expect(transactions, isNotEmpty); expect(transactions.first.id, '123'); expect(transactions.first.productId, 'product_id'); - expect( - transactions.first.expirationDate, - (321321.32 * 1000).round().toString(), - ); + expect(transactions.first.expirationDate, (321321.32 * 1000).round().toString()); }); test('should expose receiptData (JWS) in unfinished transactions', () async {