From 3ae1035123b9e445a7d4072cc6c8017b6db50357 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 30 Oct 2024 10:40:35 -0700 Subject: [PATCH 1/7] restore purchases --- .../StoreKit2/InAppPurchaseStoreKit2.swift | 31 +++++++++++-- .../StoreKit2/StoreKit2Translators.swift | 5 ++- .../Classes/StoreKit2/sk2_pigeon.g.swift | 33 +++++++++++--- .../InAppPurchaseStoreKit2PluginTests.swift | 24 ++++++++++ .../in_app_purchase_storekit_platform.dart | 6 ++- .../lib/src/sk2_pigeon.g.dart | 45 +++++++++++++++---- .../sk2_transaction_wrapper.dart | 13 ++++-- .../pigeons/sk2_pigeon.dart | 7 ++- .../test/fakes/fake_storekit_platform.dart | 22 ++++++++- ...app_purchase_storekit_2_platform_test.dart | 37 +++++++++++++++ .../test/sk2_test_api.g.dart | 30 ++++++++++++- 11 files changed, 227 insertions(+), 26 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift index cd8e96b5d135..0fe7489c7d15 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift @@ -103,6 +103,30 @@ extension InAppPurchasePlugin: InAppPurchase2API { } } + func restorePurchases(completion: @escaping (Result) -> Void) { + Task { @MainActor in + do { + for await completedPurchase in Transaction.currentEntitlements { + switch completedPurchase { + case .verified(let purchase): + self.sendTransactionUpdate( + transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)") + print("hello") + case .unverified(_, _): + completion( + .failure( + PigeonError( + code: "storekit2_restore_failed", + message: + "This purchase could not be restored.", + details: "Receipt Data : \(completedPurchase.jwsRepresentation)"))) + } + } + completion(.success(Void())) + } + } + } + /// Wrapper method around StoreKit2's finish() method https://developer.apple.com/documentation/storekit/transaction/3749694-finish func finish(id: Int64, completion: @escaping (Result) -> Void) { Task { @@ -136,9 +160,10 @@ extension InAppPurchasePlugin: InAppPurchase2API { } /// Sends an transaction back to Dart. Access these transactions with `purchaseStream` - func sendTransactionUpdate(transaction: Transaction) { - let transactionMessage = transaction.convertToPigeon() - transactionCallbackAPI?.onTransactionsUpdated(newTransaction: transactionMessage) { result in + private func sendTransactionUpdate(transaction: Transaction, receipt: String? = nil) { + let transactionMessage = transaction.convertToPigeon(receipt: receipt) + self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: [transactionMessage]) { + result in switch result { case .success: break case .failure(let error): diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift index b04caa25ae64..fd9a23480819 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift @@ -186,7 +186,7 @@ extension Product.PurchaseResult { @available(iOS 15.0, macOS 12.0, *) extension Transaction { - func convertToPigeon(restoring: Bool = false) -> SK2TransactionMessage { + func convertToPigeon(receipt: String? = nil) -> SK2TransactionMessage { let dateFromatter: DateFormatter = DateFormatter() dateFromatter.dateFormat = "yyyy-MM-dd HH:mm:ss" @@ -198,7 +198,8 @@ extension Transaction { purchaseDate: dateFromatter.string(from: purchaseDate), purchasedQuantity: Int64(purchasedQuantity), appAccountToken: appAccountToken?.uuidString, - restoring: restoring + restoring: receipt != nil, + receiptData: receipt ) } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/sk2_pigeon.g.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/sk2_pigeon.g.swift index f6ff6bfe63b3..f79f51b260de 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/sk2_pigeon.g.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/sk2_pigeon.g.swift @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -315,6 +315,7 @@ struct SK2TransactionMessage { var purchasedQuantity: Int64 var appAccountToken: String? = nil var restoring: Bool + var receiptData: String? = nil var error: SK2ErrorMessage? = nil // swift-format-ignore: AlwaysUseLowerCamelCase @@ -326,7 +327,8 @@ struct SK2TransactionMessage { let purchasedQuantity = pigeonVar_list[4] as! Int64 let appAccountToken: String? = nilOrValue(pigeonVar_list[5]) let restoring = pigeonVar_list[6] as! Bool - let error: SK2ErrorMessage? = nilOrValue(pigeonVar_list[7]) + let receiptData: String? = nilOrValue(pigeonVar_list[7]) + let error: SK2ErrorMessage? = nilOrValue(pigeonVar_list[8]) return SK2TransactionMessage( id: id, @@ -336,6 +338,7 @@ struct SK2TransactionMessage { purchasedQuantity: purchasedQuantity, appAccountToken: appAccountToken, restoring: restoring, + receiptData: receiptData, error: error ) } @@ -348,6 +351,7 @@ struct SK2TransactionMessage { purchasedQuantity, appAccountToken, restoring, + receiptData, error, ] } @@ -508,6 +512,7 @@ protocol InAppPurchase2API { func finish(id: Int64, completion: @escaping (Result) -> Void) func startListeningToTransactions() throws func stopListeningToTransactions() throws + func restorePurchases(completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -645,12 +650,30 @@ class InAppPurchase2APISetup { } else { stopListeningToTransactionsChannel.setMessageHandler(nil) } + let restorePurchasesChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.restorePurchases\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + restorePurchasesChannel.setMessageHandler { _, reply in + api.restorePurchases { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + restorePurchasesChannel.setMessageHandler(nil) + } } } /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol InAppPurchase2CallbackAPIProtocol { func onTransactionsUpdated( - newTransaction newTransactionArg: SK2TransactionMessage, + newTransactions newTransactionsArg: [SK2TransactionMessage], completion: @escaping (Result) -> Void) } class InAppPurchase2CallbackAPI: InAppPurchase2CallbackAPIProtocol { @@ -664,14 +687,14 @@ class InAppPurchase2CallbackAPI: InAppPurchase2CallbackAPIProtocol { return sk2_pigeonPigeonCodec.shared } func onTransactionsUpdated( - newTransaction newTransactionArg: SK2TransactionMessage, + newTransactions newTransactionsArg: [SK2TransactionMessage], completion: @escaping (Result) -> Void ) { let channelName: String = "dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2CallbackAPI.onTransactionsUpdated\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel( name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([newTransactionArg] as [Any?]) { response in + channel.sendMessage([newTransactionsArg] as [Any?]) { response in guard let listResponse = response as? [Any?] else { completion(.failure(createConnectionError(withChannelName: channelName))) return 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 c2fcfbdfedf0..34caa9271331 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 @@ -226,4 +226,28 @@ final class InAppPurchase2PluginTests: XCTestCase { } await fulfillment(of: [expectation], timeout: 5) } + + func testRestoreProductSuccess() async throws { + let purchaseExpectation = self.expectation(description: "Purchase request should succeed") + let restoreExpectation = self.expectation(description: "Restore request should succeed") + + plugin.purchase(id: "subscription_silver", options: nil) { result in + switch result { + case .success(_): + purchaseExpectation.fulfill() + case .failure(let error): + XCTFail("Purchase should NOT fail. Failed with \(error)") + } + } + plugin.restorePurchases { result in + switch result { + case .success(): + restoreExpectation.fulfill() + case .failure(let error): + XCTFail("Restore purchases should NOT fail. Failed with \(error)") + } + } + + await fulfillment(of: [restoreExpectation, purchaseExpectation], timeout: 5) + } } 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 f930e57a8e7a..7e44fb361fb3 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 @@ -31,7 +31,7 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { InAppPurchaseStoreKitPlatform(); /// Experimental flag for StoreKit2. - static bool _useStoreKit2 = false; + static bool _useStoreKit2 = true; /// StoreKit1 static late SKPaymentQueueWrapper _skPaymentQueueWrapper; @@ -149,6 +149,10 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { @override Future restorePurchases({String? applicationUserName}) async { + if (_useStoreKit2) { + print("platform.dart"); + return SK2Transaction.restorePurchases(); + } return _sk1transactionObserver .restoreTransactions( queue: _skPaymentQueueWrapper, 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 8c6753fdf205..ac154ddce079 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 @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -303,6 +303,7 @@ class SK2TransactionMessage { this.purchasedQuantity = 1, this.appAccountToken, this.restoring = false, + this.receiptData, this.error, }); @@ -320,6 +321,8 @@ class SK2TransactionMessage { bool restoring; + String? receiptData; + SK2ErrorMessage? error; Object encode() { @@ -331,6 +334,7 @@ class SK2TransactionMessage { purchasedQuantity, appAccountToken, restoring, + receiptData, error, ]; } @@ -345,7 +349,8 @@ class SK2TransactionMessage { purchasedQuantity: result[4]! as int, appAccountToken: result[5] as String?, restoring: result[6]! as bool, - error: result[7] as SK2ErrorMessage?, + receiptData: result[7] as String?, + error: result[8] as SK2ErrorMessage?, ); } } @@ -685,12 +690,36 @@ class InAppPurchase2API { return; } } + + Future restorePurchases() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.restorePurchases$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } abstract class InAppPurchase2CallbackAPI { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - void onTransactionsUpdated(SK2TransactionMessage newTransaction); + void onTransactionsUpdated(List newTransactions); static void setUp( InAppPurchase2CallbackAPI? api, { @@ -713,12 +742,12 @@ abstract class InAppPurchase2CallbackAPI { assert(message != null, 'Argument for dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2CallbackAPI.onTransactionsUpdated was null.'); final List args = (message as List?)!; - final SK2TransactionMessage? arg_newTransaction = - (args[0] as SK2TransactionMessage?); - assert(arg_newTransaction != null, - 'Argument for dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2CallbackAPI.onTransactionsUpdated was null, expected non-null SK2TransactionMessage.'); + final List? arg_newTransactions = + (args[0] as List?)?.cast(); + assert(arg_newTransactions != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2CallbackAPI.onTransactionsUpdated was null, expected non-null List.'); try { - api.onTransactionsUpdated(arg_newTransaction!); + api.onTransactionsUpdated(arg_newTransactions!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); 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 5afefddba7af..d92db9e8ffb8 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 @@ -86,6 +86,12 @@ class SK2Transaction { static void stopListeningToTransactions() { _hostApi.stopListeningToTransactions(); } + + /// Restore previously completed purchases. + static Future restorePurchases() async { + print("wrapper.dart"); + await _hostApi.restorePurchases(); + } } extension on SK2TransactionMessage { @@ -127,8 +133,9 @@ class SK2TransactionObserverWrapper implements InAppPurchase2CallbackAPI { final StreamController> transactionsCreatedController; @override - void onTransactionsUpdated(SK2TransactionMessage newTransaction) { - transactionsCreatedController - .add([newTransaction.convertToDetails()]); + void onTransactionsUpdated(List newTransactions) { + transactionsCreatedController.add(newTransactions + .map((SK2TransactionMessage e) => e.convertToDetails()) + .toList()); } } 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 fbf4c421ac1b..e3aa52232684 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 @@ -144,6 +144,7 @@ class SK2TransactionMessage { this.purchasedQuantity = 1, this.appAccountToken, this.error, + this.receiptData, this.restoring = false}); final int id; final int originalId; @@ -152,6 +153,7 @@ class SK2TransactionMessage { final int purchasedQuantity; final String? appAccountToken; final bool restoring; + final String? receiptData; final SK2ErrorMessage? error; } @@ -189,9 +191,12 @@ abstract class InAppPurchase2API { void startListeningToTransactions(); void stopListeningToTransactions(); + + @async + void restorePurchases(); } @FlutterApi() abstract class InAppPurchase2CallbackAPI { - void onTransactionsUpdated(SK2TransactionMessage newTransaction); + void onTransactionsUpdated(List newTransactions); } 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 29932a9f8180..06ca4a609935 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 @@ -285,7 +285,7 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi { class FakeStoreKit2Platform implements TestInAppPurchase2Api { late Set validProductIDs; late Map validProducts; - late List transactionList; + late List transactionList = []; late bool testTransactionFail; late int testTransactionCancel; late List finishedTransactions; @@ -310,6 +310,18 @@ class FakeStoreKit2Platform implements TestInAppPurchase2Api { } } + SK2TransactionMessage createRestoredTransaction( + String productId, String transactionId, + {int quantity = 1}) { + return SK2TransactionMessage( + id: 123, + originalId: 321, + productId: '', + purchaseDate: '', + appAccountToken: '', + restoring: true); + } + @override bool canMakePayments() { return true; @@ -341,7 +353,7 @@ class FakeStoreKit2Platform implements TestInAppPurchase2Api { final SK2TransactionMessage transaction = createPendingTransaction(id); InAppPurchaseStoreKitPlatform.sk2transactionObserver - .onTransactionsUpdated(transaction); + .onTransactionsUpdated([transaction]); return Future.value( SK2ProductPurchaseResultMessage.success); } @@ -371,6 +383,12 @@ class FakeStoreKit2Platform implements TestInAppPurchase2Api { void stopListeningToTransactions() { isListenerRegistered = false; } + + @override + Future restorePurchases() async { + InAppPurchaseStoreKitPlatform.sk2transactionObserver + .onTransactionsUpdated(transactionList); + } } SK2TransactionMessage createPendingTransaction(String id, {int quantity = 1}) { 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 02c80b9d9bee..6fb2ba030704 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 @@ -151,4 +151,41 @@ void main() { throwsA(isInstanceOf())); }); }); + + group('restore purchases', () { + test('should emit restored transactions on purchase stream', () async { + fakeStoreKit2Platform.transactionList + .add(fakeStoreKit2Platform.createRestoredTransaction('foo', 'RT1')); + fakeStoreKit2Platform.transactionList + .add(fakeStoreKit2Platform.createRestoredTransaction('foo', 'RT2')); + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + subscription.cancel(); + completer.complete(purchaseDetailsList); + } + }); + + await iapStoreKitPlatform.restorePurchases(); + final List details = await completer.future; + + expect(details.length, 2); + for (int i = 0; i < fakeStoreKit2Platform.transactionList.length; i++) { + final SK2TransactionMessage expected = + fakeStoreKit2Platform.transactionList[i]; + final PurchaseDetails actual = details[i]; + + expect(actual.purchaseID, expected.id.toString()); + expect(actual.verificationData, isNotNull); + expect(actual.status, PurchaseStatus.restored); + // In storekit 2, restored purchases don't have to finished. + expect(actual.pendingCompletePurchase, false); + } + }); + }); } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/sk2_test_api.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/sk2_test_api.g.dart index 58850af3ece4..5c595d45c09e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/sk2_test_api.g.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/sk2_test_api.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports @@ -132,6 +132,8 @@ abstract class TestInAppPurchase2Api { void stopListeningToTransactions(); + Future restorePurchases(); + static void setUp( TestInAppPurchase2Api? api, { BinaryMessenger? binaryMessenger, @@ -344,5 +346,31 @@ abstract class TestInAppPurchase2Api { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.restorePurchases$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + try { + await api.restorePurchases(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } From 7ed676dc53b8cf020fd4fa643925103908352e33 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 30 Oct 2024 13:02:02 -0700 Subject: [PATCH 2/7] remove print statement........... --- .../darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift index 0fe7489c7d15..82a3bedf668e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift @@ -111,7 +111,6 @@ extension InAppPurchasePlugin: InAppPurchase2API { case .verified(let purchase): self.sendTransactionUpdate( transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)") - print("hello") case .unverified(_, _): completion( .failure( From a2c1f2a1c3777151851747e05cece6472f01b944 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 30 Oct 2024 15:09:00 -0700 Subject: [PATCH 3/7] . --- .../lib/src/in_app_purchase_storekit_platform.dart | 3 +-- .../lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) 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 7e44fb361fb3..d8acdd9ebceb 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 @@ -31,7 +31,7 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { InAppPurchaseStoreKitPlatform(); /// Experimental flag for StoreKit2. - static bool _useStoreKit2 = true; + static bool _useStoreKit2 = false; /// StoreKit1 static late SKPaymentQueueWrapper _skPaymentQueueWrapper; @@ -150,7 +150,6 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { @override Future restorePurchases({String? applicationUserName}) async { if (_useStoreKit2) { - print("platform.dart"); return SK2Transaction.restorePurchases(); } return _sk1transactionObserver 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 d92db9e8ffb8..4e727dce333b 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 @@ -89,7 +89,6 @@ class SK2Transaction { /// Restore previously completed purchases. static Future restorePurchases() async { - print("wrapper.dart"); await _hostApi.restorePurchases(); } } From 62dacd8b36e45bb3a4fd8437b95651f5bcec40b7 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 31 Oct 2024 11:21:46 -0700 Subject: [PATCH 4/7] syntax...? --- .../darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift index 82a3bedf668e..241fbc6a40c3 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift @@ -104,7 +104,8 @@ extension InAppPurchasePlugin: InAppPurchase2API { } func restorePurchases(completion: @escaping (Result) -> Void) { - Task { @MainActor in + Task { + @MainActor in do { for await completedPurchase in Transaction.currentEntitlements { switch completedPurchase { From 578267714637c83e831ab2fd8c9c85cc9b987a5b Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 31 Oct 2024 11:31:25 -0700 Subject: [PATCH 5/7] syntax 2...? --- .../darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift index 241fbc6a40c3..e6fdce592876 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift @@ -103,7 +103,7 @@ extension InAppPurchasePlugin: InAppPurchase2API { } } - func restorePurchases(completion: @escaping (Result) -> Void) { + func restorePurchases(completion: @escaping (Result) -> Void) { Task { @MainActor in do { From 665ddfa19cac4169bd9183d53e21305340004969 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 31 Oct 2024 15:20:14 -0700 Subject: [PATCH 6/7] versioning --- .../in_app_purchase/in_app_purchase_storekit/CHANGELOG.md | 4 ++++ .../in_app_purchase/in_app_purchase_storekit/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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 e34d332e942f..aeb236752ec8 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,7 @@ +## 0.3.18+4 + +* Adds StoreKit 2 support for restoring purchases. + ## 0.3.18+3 * Updates Pigeon for non-nullable collection type support. 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 4c6da54a5a8a..317bc8d0e107 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.3.18+3 +version: 0.3.18+4 environment: sdk: ^3.3.0 From a2f147d2dc178055e4db7b4e03c349a278dfe1ea Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 7 Nov 2024 11:54:28 -0800 Subject: [PATCH 7/7] rename file, fix up handling for restored purchases --- ...ft => InAppPurchasePlugin+StoreKit2.swift} | 28 +++++++++++-------- .../StoreKit2/StoreKit2Translators.swift | 2 +- .../in_app_purchase_storekit_platform.dart | 2 +- .../test/fakes/fake_storekit_platform.dart | 4 +-- 4 files changed, 21 insertions(+), 15 deletions(-) rename packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/{InAppPurchaseStoreKit2.swift => InAppPurchasePlugin+StoreKit2.swift} (90%) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchasePlugin+StoreKit2.swift similarity index 90% rename from packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchasePlugin+StoreKit2.swift index e6fdce592876..47daff575302 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchaseStoreKit2.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/InAppPurchasePlugin+StoreKit2.swift @@ -96,7 +96,7 @@ extension InAppPurchasePlugin: InAppPurchase2API { @MainActor in do { let transactionsMsgs = await rawTransactions().map { - $0.convertToPigeon() + $0.convertToPigeon(receipt: nil) } completion(.success(transactionsMsgs)) } @@ -104,24 +104,30 @@ extension InAppPurchasePlugin: InAppPurchase2API { } func restorePurchases(completion: @escaping (Result) -> Void) { - Task { - @MainActor in + Task { [weak self] in + guard let self = self else { return } do { + var unverifiedPurchases: [UInt64: (receipt: String, error: Error?)] = [:] for await completedPurchase in Transaction.currentEntitlements { switch completedPurchase { case .verified(let purchase): self.sendTransactionUpdate( transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)") - case .unverified(_, _): - completion( - .failure( - PigeonError( - code: "storekit2_restore_failed", - message: - "This purchase could not be restored.", - details: "Receipt Data : \(completedPurchase.jwsRepresentation)"))) + case .unverified(let failedPurchase, let error): + unverifiedPurchases[failedPurchase.id] = ( + receipt: completedPurchase.jwsRepresentation, error: error + ) } } + if !unverifiedPurchases.isEmpty { + completion( + .failure( + PigeonError( + code: "storekit2_restore_failed", + message: + "This purchase could not be restored.", + details: unverifiedPurchases))) + } completion(.success(Void())) } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift index fd9a23480819..f2b2511afa98 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/StoreKit2/StoreKit2Translators.swift @@ -186,7 +186,7 @@ extension Product.PurchaseResult { @available(iOS 15.0, macOS 12.0, *) extension Transaction { - func convertToPigeon(receipt: String? = nil) -> SK2TransactionMessage { + func convertToPigeon(receipt: String?) -> SK2TransactionMessage { let dateFromatter: DateFormatter = DateFormatter() dateFromatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 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 d8acdd9ebceb..e74b46ddfc2f 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 @@ -51,7 +51,7 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { /// Callback handler for transaction status changes for StoreKit2 transactions @visibleForTesting - static SK2TransactionObserverWrapper get sk2transactionObserver => + static SK2TransactionObserverWrapper get sk2TransactionObserver => _sk2transactionObserver; /// Registers this class as the default instance of [InAppPurchasePlatform]. 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 06ca4a609935..55c490bb62b4 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 @@ -352,7 +352,7 @@ class FakeStoreKit2Platform implements TestInAppPurchase2Api { {SK2ProductPurchaseOptionsMessage? options}) { final SK2TransactionMessage transaction = createPendingTransaction(id); - InAppPurchaseStoreKitPlatform.sk2transactionObserver + InAppPurchaseStoreKitPlatform.sk2TransactionObserver .onTransactionsUpdated([transaction]); return Future.value( SK2ProductPurchaseResultMessage.success); @@ -386,7 +386,7 @@ class FakeStoreKit2Platform implements TestInAppPurchase2Api { @override Future restorePurchases() async { - InAppPurchaseStoreKitPlatform.sk2transactionObserver + InAppPurchaseStoreKitPlatform.sk2TransactionObserver .onTransactionsUpdated(transactionList); } }