From f1e49d105ce08cf90db136e78a7e74645e93f21f Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 15 Nov 2023 11:55:02 +0100 Subject: [PATCH 01/31] Add method to process initial sk2 transactions in observer mode --- Sources/Logging/Strings/PurchaseStrings.swift | 3 +++ Sources/Purchasing/Configuration.swift | 7 ------- Sources/Purchasing/Purchases/Purchases.swift | 10 ++++++++++ .../Purchasing/Purchases/PurchasesOrchestrator.swift | 2 +- .../StoreKit2/StoreKit2TransactionListener.swift | 8 +++++--- .../StoreKitObserverModeIntegrationTests.swift | 2 +- .../StoreKit2/StoreKit2TransactionListenerTests.swift | 7 +++++++ .../Mocks/MockStoreKit2TransactionListener.swift | 3 ++- 8 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index 66a1d353f6..8c6ffcb8e7 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -86,6 +86,7 @@ enum PurchaseStrings { case caching_presented_offering_identifier(offeringID: String, productID: String) case payment_queue_wrapper_delegate_call_sk1_enabled case restorepurchases_called_with_allow_sharing_appstore_account_false + case sk2_error_processing_observer_mode_transaction(error: Error) } @@ -329,6 +330,8 @@ extension PurchaseStrings: LogMessage { case .restorepurchases_called_with_allow_sharing_appstore_account_false: return "allowSharingAppStoreAccount is set to false and restorePurchases has been called. " + "Are you sure you want to do this?" + case let .sk2_error_processing_observer_mode_transaction(error): + return "Error processing observer mode transaction: \(error)" } } diff --git a/Sources/Purchasing/Configuration.swift b/Sources/Purchasing/Configuration.swift index d691847aff..10a2dbbcb8 100644 --- a/Sources/Purchasing/Configuration.swift +++ b/Sources/Purchasing/Configuration.swift @@ -54,7 +54,6 @@ import Foundation private init(with builder: Builder) { Self.verify(apiKey: builder.apiKey) - Self.verify(observerMode: builder.observerMode, storeKitVersion: builder.storeKitVersion) self.apiKey = builder.apiKey self.appUserID = builder.appUserID @@ -324,12 +323,6 @@ extension Configuration { } } - fileprivate static func verify(observerMode: Bool, storeKitVersion: StoreKitVersion) { - if observerMode, storeKitVersion == .storeKit2 { - Logger.warn(Strings.configure.observer_mode_with_storekit2) - } - } - private static let applePlatformKeyPrefix: String = "appl_" } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index b45ecae085..7fda8c045a 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1101,6 +1101,16 @@ public extension Purchases { #endif + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) + func processObserverModeTransaction(_ transaction: StoreKit.Product.PurchaseResult) async { + do { + _ = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( + purchaseResult: transaction, fromTransactionUpdate: true) + } catch { + Logger.warn(Strings.purchase.sk2_error_processing_observer_mode_transaction(error: error)) + } + } + } // swiftlint:enable missing_docs diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index 8cfc95583d..ec0a3cd69e 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -496,7 +496,7 @@ final class PurchasesOrchestrator { // `userCancelled` above comes from `StoreKitError.userCancelled`. // This detects if `Product.PurchaseResult.userCancelled` is true. let (userCancelled, transaction) = try await self.storeKit2TransactionListener - .handle(purchaseResult: result) + .handle(purchaseResult: result, fromTransactionUpdate: false) if userCancelled, self.systemInfo.dangerousSettings.customEntitlementComputation { throw ErrorUtils.purchaseCancelledError() diff --git a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift index ee5ac08531..0ec3ca7ffe 100644 --- a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift +++ b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -40,7 +40,8 @@ protocol StoreKit2TransactionListenerType: Sendable { /// - Returns: `nil` `CustomerInfo` if purchases were not synced /// - Throws: Error if purchase was not completed successfully func handle( - purchaseResult: StoreKit.Product.PurchaseResult + purchaseResult: StoreKit.Product.PurchaseResult, + fromTransactionUpdate: Bool ) async throws -> StoreKit2TransactionListener.ResultData } @@ -116,12 +117,13 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { /// - Returns: `nil` `CustomerInfo` if purchases were not synced /// - Throws: Error if purchase was not completed successfully func handle( - purchaseResult: StoreKit.Product.PurchaseResult + purchaseResult: StoreKit.Product.PurchaseResult, + fromTransactionUpdate: Bool = false ) async throws -> ResultData { switch purchaseResult { case let .success(verificationResult): let transaction = try await self.handle(transactionResult: verificationResult, - fromTransactionUpdate: false) + fromTransactionUpdate: fromTransactionUpdate) return (false, transaction) case .pending: throw ErrorUtils.paymentDeferredError() diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index 6def277eff..f7bd062dac 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -51,7 +51,7 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) - XCTExpectFailure("This test currently does not pass (see FB12231111)") + await Purchases.shared.processObserverModeTransaction(result) try await asyncWait( description: "Entitlement didn't become active", diff --git a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift index b5b9a803d3..a3e98ab09f 100644 --- a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift +++ b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift @@ -149,6 +149,13 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { expect(self.delegate.invokedTransactionUpdated) == false } + func testHandlePurchaseResultNotifiesDelegate() async throws { + let result = try await self.purchase().result + await Purchases.shared.processObserverModeTransaction(result) + + expect(self.delegate.invokedTransactionUpdated) == true + } + func testHandleUnverifiedPurchase() async throws { let (_, _, transaction) = try await self.purchase() diff --git a/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift b/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift index 0b5d667300..d49a8d59fe 100644 --- a/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift +++ b/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift @@ -55,7 +55,8 @@ final class MockStoreKit2TransactionListener: StoreKit2TransactionListenerType { var invokedHandleParametersList = [(purchaseResult: Box, Void)]() func handle( - purchaseResult: StoreKit.Product.PurchaseResult + purchaseResult: StoreKit.Product.PurchaseResult, + fromTransactionUpdate: Bool = false ) async throws -> StoreKit2TransactionListener.ResultData { self.invokedHandle = true self.invokedHandleCount += 1 From e988db6194e7e086640a91cecaaa9ba33dd14a9c Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 18:45:49 +0100 Subject: [PATCH 02/31] Improve error handling, add method for processing last transaction by product id and add tests --- .../Logging/Strings/ConfigureStrings.swift | 4 - Sources/Logging/Strings/PurchaseStrings.swift | 9 +- Sources/Purchasing/Purchases/Purchases.swift | 27 +++- .../StoreKit2TransactionListener.swift | 12 +- .../ObjCAPITester/RCPurchasesAPI.m | 5 +- .../SwiftAPITester/PurchasesAPI.swift | 6 + ...StoreKitObserverModeIntegrationTests.swift | 26 +++- .../StoreKit2TransactionListenerTests.swift | 127 +++++++----------- .../MockStoreKit2TransactionListener.swift | 18 +++ .../Purchasing/ConfigurationTests.swift | 44 ------ 10 files changed, 132 insertions(+), 146 deletions(-) diff --git a/Sources/Logging/Strings/ConfigureStrings.swift b/Sources/Logging/Strings/ConfigureStrings.swift index 990ab46a31..c3046f2900 100644 --- a/Sources/Logging/Strings/ConfigureStrings.swift +++ b/Sources/Logging/Strings/ConfigureStrings.swift @@ -31,8 +31,6 @@ enum ConfigureStrings { case observer_mode_enabled - case observer_mode_with_storekit2 - case response_verification_mode(Signing.ResponseVerificationMode) case storekit_version(StoreKitVersion) @@ -98,8 +96,6 @@ extension ConfigureStrings: LogMessage { return "Debug logging enabled" case .observer_mode_enabled: return "Purchases is configured in observer mode" - case .observer_mode_with_storekit2: - return "Observer mode is not currently compatible with StoreKit 2" case let .response_verification_mode(mode): switch mode { case .disabled: diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index 8c6ffcb8e7..0c78bc4e48 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -86,7 +86,8 @@ enum PurchaseStrings { case caching_presented_offering_identifier(offeringID: String, productID: String) case payment_queue_wrapper_delegate_call_sk1_enabled case restorepurchases_called_with_allow_sharing_appstore_account_false - case sk2_error_processing_observer_mode_transaction(error: Error) + case sk2_observer_mode_error_processing_transaction(error: Error) + case sk2_observer_mode_missing_transaction_for_product(productID: String) } @@ -330,8 +331,10 @@ extension PurchaseStrings: LogMessage { case .restorepurchases_called_with_allow_sharing_appstore_account_false: return "allowSharingAppStoreAccount is set to false and restorePurchases has been called. " + "Are you sure you want to do this?" - case let .sk2_error_processing_observer_mode_transaction(error): - return "Error processing observer mode transaction: \(error)" + case let .sk2_observer_mode_error_processing_transaction(error): + return "Obserber mode could not process transaction: \(error)" + case let .sk2_observer_mode_missing_transaction_for_product(productID): + return "Observer mode could not find transaction for product ID: \(productID)" } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 7fda8c045a..1b4b248c29 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1101,14 +1101,27 @@ public extension Purchases { #endif - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) - func processObserverModeTransaction(_ transaction: StoreKit.Product.PurchaseResult) async { - do { - _ = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( - purchaseResult: transaction, fromTransactionUpdate: true) - } catch { - Logger.warn(Strings.purchase.sk2_error_processing_observer_mode_transaction(error: error)) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func processObserverModeTransaction( + purchaseResult: StoreKit.Product.PurchaseResult + ) async throws -> StoreTransaction? { + let (userCancelled, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( + purchaseResult: purchaseResult, fromTransactionUpdate: true) + + if userCancelled, self.systemInfo.dangerousSettings.customEntitlementComputation { + throw NewErrorUtils.purchaseCancelledError() + } + return transaction + } + + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @objc func processObserverModeTransaction(productID: String) async throws -> StoreTransaction? { + guard let transaction = await StoreKit.Transaction.latest(for: productID) else { + Logger.warn(Strings.purchase.sk2_observer_mode_missing_transaction_for_product(productID: productID)) + return nil } + return try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( + transactionResult: transaction, fromTransactionUpdate: true) } } diff --git a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift index 0ec3ca7ffe..4cc517bc4e 100644 --- a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift +++ b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -44,6 +44,13 @@ protocol StoreKit2TransactionListenerType: Sendable { fromTransactionUpdate: Bool ) async throws -> StoreKit2TransactionListener.ResultData + /// - Returns: `nil` `CustomerInfo` if purchases were not synced + /// - Throws: Error if purchase was not completed successfully + func handle( + transactionResult: StoreKit.VerificationResult, + fromTransactionUpdate: Bool + ) async throws -> StoreTransaction + } /// Observes `StoreKit.Transaction.updates`, which receives: @@ -137,11 +144,6 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { } } -} - -@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) -private extension StoreKit2TransactionListener { - /// - Throws: ``ErrorCode`` if the transaction fails to verify. /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. func handle( diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index 09e4d3a503..aa29ca7a6a 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -164,7 +164,10 @@ + (void)checkAPI { [p purchasePackage:pack withCompletion:^(RCStoreTransaction *t, RCCustomerInfo *i, NSError *e, BOOL userCancelled) { }]; [p restorePurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; [p syncPurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; - + if (@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)) { + [p processObserverModeTransactionWithProductID:@"" completionHandler:^(RCStoreTransaction *t, NSError *error) {}]; + } + [p checkTrialOrIntroDiscountEligibilityForProduct:storeProduct completion:^(RCIntroEligibilityStatus status) { }]; [p checkTrialOrIntroDiscountEligibility:@[@""] completion:^(NSDictionary *d) { }]; if (@available(iOS 12.2, macOS 10.14.4, macCatalyst 13.0, tvOS 12.2, watchOS 6.2, *)) { diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 06b1d98241..7aa5eb338a 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -263,6 +263,12 @@ private func checkAsyncMethods(purchases: Purchases) async { let _: CustomerInfo = try await purchases.restorePurchases() let _: CustomerInfo = try await purchases.syncPurchases() + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + let result = try await StoreKit.Product.products(for: [""]).first!.purchase() + let _: StoreTransaction? = try await purchases.processObserverModeTransaction(purchaseResult: result) + let _: StoreTransaction? = try await purchases.processObserverModeTransaction(productID: "") + } + for try await _: CustomerInfo in purchases.customerInfoStream {} #if os(iOS) || VISION_OS diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index f7bd062dac..a6ed270c6d 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -45,13 +45,35 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes } @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) - func testPurchaseInDevicePostsReceipt() async throws { + func testObservingTransactionPurchaseResultUnlocksEntitlement() async throws { let result = try await self.manager.purchaseProductFromStoreKit2() let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) - await Purchases.shared.processObserverModeTransaction(result) + _ = try await Purchases.shared.processObserverModeTransaction(purchaseResult: result) + + try await asyncWait( + description: "Entitlement didn't become active", + timeout: .seconds(5), + pollInterval: .milliseconds(500) + ) { + let entitlement = await self.purchasesDelegate + .customerInfo? + .entitlements[Self.entitlementIdentifier] + + return entitlement?.isActive == true + } + } + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testObservingTransactionWithProductIDUnlocksEntitlement() async throws { + let result = try await self.manager.purchaseProductFromStoreKit2() + let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) + + try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) + + _ = try await Purchases.shared.processObserverModeTransaction(productID: transaction.productID) try await asyncWait( description: "Entitlement didn't become active", diff --git a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift index a3e98ab09f..237b2c1fca 100644 --- a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift +++ b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift @@ -69,9 +69,9 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { expect(handle?.isCancelled) == true } - // MARK: - + // MARK: - Purchase Result Tests - func testVerifiedTransactionReturnsOriginalTransaction() async throws { + func testVerifiedTransactoinInPurchaseResultReturnsOriginalTransaction() async throws { try AvailabilityChecks.iOS15APIAvailableOrSkipTest() let fakeTransaction = try await self.simulateAnyPurchase() @@ -104,7 +104,7 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { } } - func testUnverifiedTransactionsReturnStoreProblemError() async throws { + func testUnverifiedTransactionsInPurchaseResultReturnStoreProblemError() async throws { try AvailabilityChecks.iOS15APIAvailableOrSkipTest() let transaction = try await self.simulateAnyPurchase() @@ -149,13 +149,6 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { expect(self.delegate.invokedTransactionUpdated) == false } - func testHandlePurchaseResultNotifiesDelegate() async throws { - let result = try await self.purchase().result - await Purchases.shared.processObserverModeTransaction(result) - - expect(self.delegate.invokedTransactionUpdated) == true - } - func testHandleUnverifiedPurchase() async throws { let (_, _, transaction) = try await self.purchase() @@ -189,101 +182,75 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { } } -} + // MARK: - Transaction Result Tests -// MARK: - Transaction.updates tests - -@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) -class StoreKit2TransactionListenerTransactionUpdatesTests: StoreKit2TransactionListenerBaseTests { + func testVerifiedTransactionReturnsOriginalTransaction() async throws { + try AvailabilityChecks.iOS15APIAvailableOrSkipTest() - func testPurchasingInTheAppDoesNotNotifyDelegate() async throws { - await self.listener.listenForTransactions() + let fakeTransaction = try await self.simulateAnyPurchase() - try await self.simulateAnyPurchase(finishTransaction: true) - try await self.verifyTransactionsWereNotUpdated() + let transaction = try await self.listener.handle(transactionResult: fakeTransaction, + fromTransactionUpdate: false) + expect(transaction.sk2Transaction) == fakeTransaction.underlyingTransaction } - func testPurchasingOutsideTheAppNotifiesDelegate() async throws { - await self.listener.listenForTransactions() + func testUnverifiedTransactionsReturnStoreProblemError() async throws { + try AvailabilityChecks.iOS15APIAvailableOrSkipTest() - try self.testSession.buyProduct(productIdentifier: Self.productID) + let transaction = try await self.simulateAnyPurchase() + let error: StoreKit.VerificationResult.VerificationError = .invalidSignature + let result: StoreKit.VerificationResult = .unverified(transaction.underlyingTransaction, error) - try await asyncWait { - await self.delegate.invokedTransactionUpdated == true + // Note: can't use `expect().to(throwError)` or `XCTAssertThrowsError` + // because neither of them accept `async` + do { + _ = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: false) + XCTFail("Error expected") + } catch { + expect(error).to(matchError(ErrorCode.storeProblemError)) } } - func testNotifiesDelegateForExistingTransactions() async throws { - try self.testSession.buyProduct(productIdentifier: Self.productID) + func testHandleTransactionDoesNotFinishTransaction() async throws { + let (_, result, _) = try await self.purchase() - await self.listener.listenForTransactions() + let resultData = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: false) + expect(resultData.sk2Transaction) == result.underlyingTransaction - try await asyncWait { - await self.delegate.invokedTransactionUpdated == true - } + try await self.verifyUnfinishedTransaction(withId: result.underlyingTransaction.id) } - @available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) - func testNotifiesDelegateForRenewals() async throws { - try await self.simulateAnyPurchase(finishTransaction: true) + func testHandleTransactionResultDoesNotNotifyDelegate() async throws { + let result = try await self.purchase().verificationResult + _ = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: false) - await self.listener.listenForTransactions() - - try? self.testSession.forceRenewalOfSubscription(productIdentifier: Self.productID) - - try await self.waitForTransactionUpdated() - - expect(self.delegate.updatedTransactions) - .to(containElementSatisfying { transaction in - transaction.productIdentifier == Self.productID - }) - - self.logger.verifyMessageWasLogged(Strings.purchase.sk2_transactions_update_received_transaction( - productID: Self.productID - )) + expect(self.delegate.invokedTransactionUpdated) == false } -} - -// MARK: - Tests with custom stream + func testHandleTransactoinResultNotifiesDelegate() async throws { + let result = try await self.purchase().verificationResult + _ = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: true) -@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) -class StoreKit2TransactionListenerCustomStreamTests: StoreKit2TransactionListenerBaseTests { - - override var updates: AsyncStream { - get async throws { - return MockAsyncSequence(with: [ - .verified(try await self.createTransactionWithPurchase()), - .verified(try await self.createTransactionWithPurchase()), - .unverified( - try await self.createTransactionWithPurchase(), - .revokedCertificate - ) - ]) - .toAsyncStream() - } + expect(self.delegate.invokedTransactionUpdated) == true } - func testHandlesAllVerifiedTransactions() async throws { - await self.listener.listenForTransactions() - - try await asyncWait { - return await self.delegate.updatedTransactions.count == 2 - } - } + func testHandleUnverifiedTransaction() async throws { + let (_, _, transaction) = try await self.purchase() - func testHandlesTransactionsAsynchronously() async throws { - self.delegate.fakeHandlingDelay = .milliseconds(50) + let verificationError: StoreKit.VerificationResult.VerificationError = .invalidSignature - await self.listener.listenForTransactions() + do { + _ = try await self.listener.handle( + transactionResult: .unverified(transaction, verificationError), fromTransactionUpdate: false + ) + fail("Expected error") + } catch { + expect(error).to(matchError(ErrorCode.storeProblemError)) - try await asyncWait { - return await self.delegate.updatedTransactions.count == 2 + let underlyingError = try XCTUnwrap((error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError) + expect(underlyingError).to(matchError(verificationError)) } - - expect(self.delegate.receivedConcurrentRequest) == true } - } @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) diff --git a/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift b/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift index d49a8d59fe..3fe75b6c2e 100644 --- a/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift +++ b/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift @@ -71,6 +71,24 @@ final class MockStoreKit2TransactionListener: StoreKit2TransactionListenerType { return (self.mockCancelled, transaction) } + + func handle( + transactionResult: StoreKit.VerificationResult, + fromTransactionUpdate: Bool = false + ) async throws -> StoreTransaction { + self.invokedHandle = true + self.invokedHandleCount += 1 + + let transaction: StoreTransaction = self.mockTransaction.value.map { + StoreTransaction(sk2Transaction: $0, + jwsRepresentation: self.mockJWSToken, + environmentOverride: self.mockEnvironment) + } ?? StoreTransaction(sk2Transaction: transactionResult.underlyingTransaction, + jwsRepresentation: self.mockJWSToken, + environmentOverride: self.mockEnvironment) + + return transaction + } } @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) diff --git a/Tests/UnitTests/Purchasing/ConfigurationTests.swift b/Tests/UnitTests/Purchasing/ConfigurationTests.swift index c7c25e84e9..e5b5fc5e2c 100644 --- a/Tests/UnitTests/Purchasing/ConfigurationTests.swift +++ b/Tests/UnitTests/Purchasing/ConfigurationTests.swift @@ -31,50 +31,6 @@ class ConfigurationTests: TestCase { expect(Configuration.validate(apiKey: "swRTCezdEzjnJSxdexDNJfcfiFrMXwqZ")) == .legacy } - func testNoObserverModeWithStoreKit1() { - let configuration = Configuration.Builder(withAPIKey: "test").build() - - expect(configuration.observerMode) == false - expect(configuration.storeKitVersion) == .storeKit1 - - self.logger.verifyMessageWasNotLogged(Strings.configure.observer_mode_with_storekit2) - } - - func testNoObserverModeWithStoreKit2() { - let configuration = Configuration.Builder(withAPIKey: "test") - .with(storeKitVersion: .storeKit2) - .build() - - expect(configuration.observerMode) == false - expect(configuration.storeKitVersion) == .storeKit2 - - self.logger.verifyMessageWasNotLogged(Strings.configure.observer_mode_with_storekit2) - } - - func testObserverModeWithStoreKit1() { - let configuration = Configuration.Builder(withAPIKey: "test") - .with(observerMode: true) - .build() - - expect(configuration.observerMode) == true - expect(configuration.storeKitVersion) == .storeKit1 - - self.logger.verifyMessageWasNotLogged(Strings.configure.observer_mode_with_storekit2) - } - - func testObserverModeWithStoreKit2() { - let configuration = Configuration.Builder(withAPIKey: "test") - .with(observerMode: true) - .with(storeKitVersion: .storeKit2) - .build() - - expect(configuration.observerMode) == true - expect(configuration.storeKitVersion) == .storeKit2 - - self.logger.verifyMessageWasLogged(Strings.configure.observer_mode_with_storekit2, - level: .warn) - } - func testStoreKitVersionUsesStoreKit1ByDefault() { let configuration = Configuration.Builder(withAPIKey: "test") .build() From 762df7d8b22750a998b5f61178a1baafcb358099 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 18:48:54 +0100 Subject: [PATCH 03/31] process -> handle --- Sources/Purchasing/Purchases/Purchases.swift | 4 ++-- Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m | 2 +- .../SwiftAPITester/SwiftAPITester/PurchasesAPI.swift | 4 ++-- .../StoreKitObserverModeIntegrationTests.swift | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 1b4b248c29..bd59b9353d 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1102,7 +1102,7 @@ public extension Purchases { #endif @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) - func processObserverModeTransaction( + func handleObserverModeTransaction( purchaseResult: StoreKit.Product.PurchaseResult ) async throws -> StoreTransaction? { let (userCancelled, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( @@ -1115,7 +1115,7 @@ public extension Purchases { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) - @objc func processObserverModeTransaction(productID: String) async throws -> StoreTransaction? { + @objc func handleObserverModeTransaction(productID: String) async throws -> StoreTransaction? { guard let transaction = await StoreKit.Transaction.latest(for: productID) else { Logger.warn(Strings.purchase.sk2_observer_mode_missing_transaction_for_product(productID: productID)) return nil diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index aa29ca7a6a..93a052b732 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -165,7 +165,7 @@ + (void)checkAPI { [p restorePurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; [p syncPurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; if (@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)) { - [p processObserverModeTransactionWithProductID:@"" completionHandler:^(RCStoreTransaction *t, NSError *error) {}]; + [p handleObserverModeTransactionWithProductID:@"" completionHandler:^(RCStoreTransaction *t, NSError *error) {}]; } [p checkTrialOrIntroDiscountEligibilityForProduct:storeProduct completion:^(RCIntroEligibilityStatus status) { }]; diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 7aa5eb338a..6c11cfeb41 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -265,8 +265,8 @@ private func checkAsyncMethods(purchases: Purchases) async { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { let result = try await StoreKit.Product.products(for: [""]).first!.purchase() - let _: StoreTransaction? = try await purchases.processObserverModeTransaction(purchaseResult: result) - let _: StoreTransaction? = try await purchases.processObserverModeTransaction(productID: "") + let _: StoreTransaction? = try await purchases.handleObserverModeTransaction(purchaseResult: result) + let _: StoreTransaction? = try await purchases.handleObserverModeTransaction(productID: "") } for try await _: CustomerInfo in purchases.customerInfoStream {} diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index a6ed270c6d..8f276df69c 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -51,7 +51,7 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) - _ = try await Purchases.shared.processObserverModeTransaction(purchaseResult: result) + _ = try await Purchases.shared.handleObserverModeTransaction(purchaseResult: result) try await asyncWait( description: "Entitlement didn't become active", @@ -73,7 +73,7 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) - _ = try await Purchases.shared.processObserverModeTransaction(productID: transaction.productID) + _ = try await Purchases.shared.handleObserverModeTransaction(productID: transaction.productID) try await asyncWait( description: "Entitlement didn't become active", From 0b62e265fe15c4284cb8baaa16ea371555768400 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 19:32:22 +0100 Subject: [PATCH 04/31] add back removed tests --- .../StoreKit2TransactionListenerTests.swift | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift index 237b2c1fca..fd664e154b 100644 --- a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift +++ b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift @@ -251,6 +251,102 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { expect(underlyingError).to(matchError(verificationError)) } } + +} + +// MARK: - Transaction.updates tests + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +class StoreKit2TransactionListenerTransactionUpdatesTests: StoreKit2TransactionListenerBaseTests { + + func testPurchasingInTheAppDoesNotNotifyDelegate() async throws { + await self.listener.listenForTransactions() + + try await self.simulateAnyPurchase(finishTransaction: true) + try await self.verifyTransactionsWereNotUpdated() + } + + func testPurchasingOutsideTheAppNotifiesDelegate() async throws { + await self.listener.listenForTransactions() + + try self.testSession.buyProduct(productIdentifier: Self.productID) + + try await asyncWait { + await self.delegate.invokedTransactionUpdated == true + } + } + + func testNotifiesDelegateForExistingTransactions() async throws { + try self.testSession.buyProduct(productIdentifier: Self.productID) + + await self.listener.listenForTransactions() + + try await asyncWait { + await self.delegate.invokedTransactionUpdated == true + } + } + + @available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) + func testNotifiesDelegateForRenewals() async throws { + try await self.simulateAnyPurchase(finishTransaction: true) + + await self.listener.listenForTransactions() + + try? self.testSession.forceRenewalOfSubscription(productIdentifier: Self.productID) + + try await self.waitForTransactionUpdated() + + expect(self.delegate.updatedTransactions) + .to(containElementSatisfying { transaction in + transaction.productIdentifier == Self.productID + }) + + self.logger.verifyMessageWasLogged(Strings.purchase.sk2_transactions_update_received_transaction( + productID: Self.productID + )) + } + +} + +// MARK: - Tests with custom stream + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +class StoreKit2TransactionListenerCustomStreamTests: StoreKit2TransactionListenerBaseTests { + + override var updates: AsyncStream { + get async throws { + return MockAsyncSequence(with: [ + .verified(try await self.createTransactionWithPurchase()), + .verified(try await self.createTransactionWithPurchase()), + .unverified( + try await self.createTransactionWithPurchase(), + .revokedCertificate + ) + ]) + .toAsyncStream() + } + } + + func testHandlesAllVerifiedTransactions() async throws { + await self.listener.listenForTransactions() + + try await asyncWait { + return await self.delegate.updatedTransactions.count == 2 + } + } + + func testHandlesTransactionsAsynchronously() async throws { + self.delegate.fakeHandlingDelay = .milliseconds(50) + + await self.listener.listenForTransactions() + + try await asyncWait { + return await self.delegate.updatedTransactions.count == 2 + } + + expect(self.delegate.receivedConcurrentRequest) == true + } + } @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) From d2dba8845813af9235c48ea661d3962391ca8d5c Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:04:34 +0100 Subject: [PATCH 05/31] Remove method to handle purchases by product id, add documentation, throw if observer mode is not enabled --- Sources/Error Handling/ErrorCode.swift | 5 ++ Sources/Error Handling/ErrorUtils.swift | 17 +++++- Sources/Purchasing/Purchases/Purchases.swift | 13 ++--- .../Purchasing/Purchases/PurchasesType.swift | 27 ++++++++++ .../ObjCAPITester/RCPurchasesAPI.m | 3 -- .../ObjCAPITester/RCPurchasesErrorCodeAPI.m | 1 + .../SwiftAPITester/ErrorCodesAPI.swift | 3 +- .../SwiftAPITester/PurchasesAPI.swift | 1 - ...StoreKitObserverModeIntegrationTests.swift | 53 ++++++++++--------- Tests/UnitTests/Mocks/MockPurchases.swift | 8 +++ .../UnitTests/Purchasing/ErrorCodeTests.swift | 2 +- 11 files changed, 92 insertions(+), 41 deletions(-) diff --git a/Sources/Error Handling/ErrorCode.swift b/Sources/Error Handling/ErrorCode.swift index 920d39e7ab..8a31960201 100644 --- a/Sources/Error Handling/ErrorCode.swift +++ b/Sources/Error Handling/ErrorCode.swift @@ -60,6 +60,7 @@ import Foundation @objc(RCFeatureNotAvailableInCustomEntitlementsComputationMode) case featureNotAvailableInCustomEntitlementsComputationMode = 36 @objc(RCSignatureVerificationFailed) case signatureVerificationFailed = 37 + @objc(RCObserberModeNotEnabledError) case observerModeNotEnabledError = 38 // swiftlint:enable missing_docs @@ -180,6 +181,8 @@ extension ErrorCode: DescribableError { return "This feature is not available when utilizing the customEntitlementsComputation dangerousSetting." case .signatureVerificationFailed: return "Request failed signature verification. See https://rev.cat/trusted-entitlements for more info." + case .observerModeNotEnabledError: + return "Observer mode is not enabled. You must enable Observer Mode when initializing the SDKs." @unknown default: return "Something went wrong." @@ -281,6 +284,8 @@ extension ErrorCode { return "FEATURE_NOT_AVAILABLE_IN_CUSTOM_ENTITLEMENTS_COMPUTATION_MODE_ERROR" case .signatureVerificationFailed: return "SIGNATURE_VERIFICATION_FAILED" + case .observerModeNotEnabledError: + return "OBSERVER_MODE_NOT_ENABLED" @unknown default: return "UNRECOGNIZED_ERROR" } diff --git a/Sources/Error Handling/ErrorUtils.swift b/Sources/Error Handling/ErrorUtils.swift index dc8ab2ef19..45d062ba01 100644 --- a/Sources/Error Handling/ErrorUtils.swift +++ b/Sources/Error Handling/ErrorUtils.swift @@ -78,6 +78,20 @@ enum ErrorUtils { ) } + /** + * Constructs an NSError with the ``ErrorCode/observerModeNotEnabledError`` code. + */ + static func observerModeNotEnabledError( + error: Error? = nil, + fileName: String = #fileID, functionName: String = #function, line: UInt = #line + ) -> PurchasesError { + let errorCode = ErrorCode.observerModeNotEnabledError + return ErrorUtils.error(with: errorCode, + message: errorCode.description, + underlyingError: error, + fileName: fileName, functionName: functionName, line: line) + } + /** * Maps a ``BackendErrorCode`` code to a ``ErrorCode``. code. Constructs an Error with the mapped code and adds a * `NSUnderlyingErrorKey` in the `NSError.userInfo` dictionary. The backend error code will be mapped using @@ -684,7 +698,8 @@ private extension ErrorUtils { .invalidPromotionalOfferError, .offlineConnectionError, .featureNotAvailableInCustomEntitlementsComputationMode, - .signatureVerificationFailed: + .signatureVerificationFailed, + .observerModeNotEnabledError: Logger.error( localizedDescription, fileName: fileName, diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index bd59b9353d..6b712dd56e 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1105,6 +1105,9 @@ public extension Purchases { func handleObserverModeTransaction( purchaseResult: StoreKit.Product.PurchaseResult ) async throws -> StoreTransaction? { + guard !self.systemInfo.observerMode else { + throw NewErrorUtils.observerModeNotEnabledError() + } let (userCancelled, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( purchaseResult: purchaseResult, fromTransactionUpdate: true) @@ -1114,16 +1117,6 @@ public extension Purchases { return transaction } - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) - @objc func handleObserverModeTransaction(productID: String) async throws -> StoreTransaction? { - guard let transaction = await StoreKit.Transaction.latest(for: productID) else { - Logger.warn(Strings.purchase.sk2_observer_mode_missing_transaction_for_product(productID: productID)) - return nil - } - return try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( - transactionResult: transaction, fromTransactionUpdate: true) - } - } // swiftlint:enable missing_docs diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 2289a86e91..f880780775 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -12,6 +12,7 @@ // Created by Nacho Soto on 9/20/22. import Foundation +import StoreKit // swiftlint:disable file_length @@ -1008,6 +1009,32 @@ public protocol PurchasesSwiftType: AnyObject { #endif + /** + * Use this method only if you already have your own IAP implementation using StoreKit 2 and want to use + * RevenueCat's backend. If you are using StoreKit 1 for your implementation, you do not need this method. + * + * You only need to use this method with *new* purchases. Subscription updates and observed automatically. + * + * #### Example: + * + * ```swift + * let result = try await product.purchase(options: options) + * Purchases.shared.handleObserverModeTransaction(purchaseResult: result) + * ``` + * + * - Note: You need to finish the transaction yourself after calling this method. + * + * - Parameter productID: The ID of the product that was just purchased + * + * - Throws: An error of type ``ErrorCode`` is thrown if a failure occurs while handling the purchase. + * + * - Returns: A ``StoreTransaction`` if there was a transacton found and handled for the provided product ID. + */ + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func handleObserverModeTransaction( + purchaseResult: StoreKit.Product.PurchaseResult + ) async throws -> StoreTransaction? + } // MARK: - diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index 93a052b732..e3dae8729d 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -164,9 +164,6 @@ + (void)checkAPI { [p purchasePackage:pack withCompletion:^(RCStoreTransaction *t, RCCustomerInfo *i, NSError *e, BOOL userCancelled) { }]; [p restorePurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; [p syncPurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; - if (@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)) { - [p handleObserverModeTransactionWithProductID:@"" completionHandler:^(RCStoreTransaction *t, NSError *error) {}]; - } [p checkTrialOrIntroDiscountEligibilityForProduct:storeProduct completion:^(RCIntroEligibilityStatus status) { }]; [p checkTrialOrIntroDiscountEligibility:@[@""] completion:^(NSDictionary *d) { }]; diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m index 568fbefaaa..3d434b090e 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m @@ -58,6 +58,7 @@ + (void)checkEnums { case RCInvalidPromotionalOfferError: case RCOfflineConnectionError: case RCSignatureVerificationFailed: + case RCObserberModeNotEnabledError: NSLog(@"%ld", (long)errCode); case RCFeatureNotAvailableInCustomEntitlementsComputationMode: break; diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift index 3ec4270085..38aa5cf441 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift @@ -53,7 +53,8 @@ func checkPurchasesErrorCodeEnums() { .invalidPromotionalOfferError, .offlineConnectionError, .featureNotAvailableInCustomEntitlementsComputationMode, - .signatureVerificationFailed: + .signatureVerificationFailed, + .observerModeNotEnabledError: print(errCode!) @unknown default: fatalError() diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 6c11cfeb41..8ca44f9aea 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -266,7 +266,6 @@ private func checkAsyncMethods(purchases: Purchases) async { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { let result = try await StoreKit.Product.products(for: [""]).first!.purchase() let _: StoreTransaction? = try await purchases.handleObserverModeTransaction(purchaseResult: result) - let _: StoreTransaction? = try await purchases.handleObserverModeTransaction(productID: "") } for try await _: CustomerInfo in purchases.customerInfoStream {} diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index 8f276df69c..06e9959ff3 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -12,7 +12,7 @@ // Created by Nacho Soto on 12/15/22. import Foundation - +import Nimble @testable import RevenueCat import StoreKit import StoreKitTest @@ -45,7 +45,7 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes } @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) - func testObservingTransactionPurchaseResultUnlocksEntitlement() async throws { + func testObservingTransactionUnlocksEntitlement() async throws { let result = try await self.manager.purchaseProductFromStoreKit2() let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) @@ -66,28 +66,6 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes } } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) - func testObservingTransactionWithProductIDUnlocksEntitlement() async throws { - let result = try await self.manager.purchaseProductFromStoreKit2() - let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) - - try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) - - _ = try await Purchases.shared.handleObserverModeTransaction(productID: transaction.productID) - - try await asyncWait( - description: "Entitlement didn't become active", - timeout: .seconds(5), - pollInterval: .milliseconds(500) - ) { - let entitlement = await self.purchasesDelegate - .customerInfo? - .entitlements[Self.entitlementIdentifier] - - return entitlement?.isActive == true - } - } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func testRenewalsPostReceipt() async throws { self.testSession.timeRate = .realTime @@ -103,6 +81,33 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes } +class StoreKit2ObserverModeDisabledIntegrationTests: StoreKit1ObserverModeIntegrationTests { + + override class var storeKitVersion: StoreKitVersion { .storeKit2 } + + override class var observerMode: Bool { false } + + override func setUp() async throws { + try await super.setUp() + + try AvailabilityChecks.iOS15APIAvailableOrSkipTest() + } + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testObservingTransactionThrowsIfObserverModeNotEnabled() async throws { + let result = try await self.manager.purchaseProductFromStoreKit2() + let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) + + do { + _ = try await Purchases.shared.handleObserverModeTransaction(purchaseResult: result) + fail("Expected error") + } catch { + expect(error).to(matchError(ErrorCode.observerModeNotEnabledError)) + } + } + +} + class StoreKit1ObserverModeIntegrationTests: BaseStoreKitObserverModeIntegrationTests { override class var storeKitVersion: StoreKitVersion { .storeKit1 } diff --git a/Tests/UnitTests/Mocks/MockPurchases.swift b/Tests/UnitTests/Mocks/MockPurchases.swift index 030472b3a8..f60c4234a9 100644 --- a/Tests/UnitTests/Mocks/MockPurchases.swift +++ b/Tests/UnitTests/Mocks/MockPurchases.swift @@ -12,6 +12,7 @@ // Created by Nacho Soto on 10/10/22. @testable import RevenueCat +import StoreKit final class MockPurchases { @@ -457,6 +458,13 @@ extension MockPurchases: PurchasesSwiftType { self.unimplemented() } + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func handleObserverModeTransaction( + purchaseResult: Product.PurchaseResult + ) async throws -> RevenueCat.StoreTransaction? { + self.unimplemented() + } + #if os(iOS) || targetEnvironment(macCatalyst) || VISION_OS @available(iOS 16.0, *) diff --git a/Tests/UnitTests/Purchasing/ErrorCodeTests.swift b/Tests/UnitTests/Purchasing/ErrorCodeTests.swift index 2c68dac09f..26ecbb5926 100644 --- a/Tests/UnitTests/Purchasing/ErrorCodeTests.swift +++ b/Tests/UnitTests/Purchasing/ErrorCodeTests.swift @@ -179,7 +179,7 @@ class ErrorCodeTests: TestCase { } func testErrorCodeEnumCasesAreCoveredInTests() { - expect(ErrorCode.allCases).to(haveCount(37)) + expect(ErrorCode.allCases).to(haveCount(38)) } func ensureEnumCaseMatchesExpectedRawValue(errorCode: ErrorCode, expectedRawValue: Int) { From 7a274b6c5354b4643cd85d3d63a5dd124ecbd3fa Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:05:46 +0100 Subject: [PATCH 06/31] remove unused string --- Sources/Logging/Strings/PurchaseStrings.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index 0c78bc4e48..36fcf211ac 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -87,7 +87,6 @@ enum PurchaseStrings { case payment_queue_wrapper_delegate_call_sk1_enabled case restorepurchases_called_with_allow_sharing_appstore_account_false case sk2_observer_mode_error_processing_transaction(error: Error) - case sk2_observer_mode_missing_transaction_for_product(productID: String) } @@ -333,8 +332,6 @@ extension PurchaseStrings: LogMessage { "Are you sure you want to do this?" case let .sk2_observer_mode_error_processing_transaction(error): return "Obserber mode could not process transaction: \(error)" - case let .sk2_observer_mode_missing_transaction_for_product(productID): - return "Observer mode could not find transaction for product ID: \(productID)" } } From ebb32b625d1ee34ee0f69196d5431b5b7c5f145a Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:23:19 +0100 Subject: [PATCH 07/31] do not throw purchaseCancelledError --- Sources/Purchasing/Purchases/Purchases.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 6b712dd56e..6d9dfb63d2 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1108,12 +1108,8 @@ public extension Purchases { guard !self.systemInfo.observerMode else { throw NewErrorUtils.observerModeNotEnabledError() } - let (userCancelled, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( + let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( purchaseResult: purchaseResult, fromTransactionUpdate: true) - - if userCancelled, self.systemInfo.dangerousSettings.customEntitlementComputation { - throw NewErrorUtils.purchaseCancelledError() - } return transaction } From 87a431a30d9a12e14b30733e62da2c465edc63fb Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:23:50 +0100 Subject: [PATCH 08/31] typos & docs --- Sources/Purchasing/Purchases/PurchasesType.swift | 4 ++-- .../StoreKit2/StoreKit2TransactionListener.swift | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index f880780775..0c5c65d124 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -1013,7 +1013,7 @@ public protocol PurchasesSwiftType: AnyObject { * Use this method only if you already have your own IAP implementation using StoreKit 2 and want to use * RevenueCat's backend. If you are using StoreKit 1 for your implementation, you do not need this method. * - * You only need to use this method with *new* purchases. Subscription updates and observed automatically. + * You only need to use this method with *new* purchases. Subscription updates are observed automatically. * * #### Example: * @@ -1024,7 +1024,7 @@ public protocol PurchasesSwiftType: AnyObject { * * - Note: You need to finish the transaction yourself after calling this method. * - * - Parameter productID: The ID of the product that was just purchased + * - Parameter purchaseResult: The ``StoreKit.Product.PurchaseResult`` of the product that was just purchased. * * - Throws: An error of type ``ErrorCode`` is thrown if a failure occurs while handling the purchase. * diff --git a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift index 4cc517bc4e..c0b4045cd9 100644 --- a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift +++ b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -37,15 +37,15 @@ protocol StoreKit2TransactionListenerType: Sendable { func set(delegate: StoreKit2TransactionListenerDelegate) async - /// - Returns: `nil` `CustomerInfo` if purchases were not synced - /// - Throws: Error if purchase was not completed successfully + /// - Throws: ``ErrorCode`` if purchase was not completed successfully + /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. func handle( purchaseResult: StoreKit.Product.PurchaseResult, fromTransactionUpdate: Bool ) async throws -> StoreKit2TransactionListener.ResultData - /// - Returns: `nil` `CustomerInfo` if purchases were not synced - /// - Throws: Error if purchase was not completed successfully + /// - Throws: ``ErrorCode`` if the transaction fails to verify. + /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. func handle( transactionResult: StoreKit.VerificationResult, fromTransactionUpdate: Bool @@ -121,8 +121,6 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { self.taskHandle = nil } - /// - Returns: `nil` `CustomerInfo` if purchases were not synced - /// - Throws: Error if purchase was not completed successfully func handle( purchaseResult: StoreKit.Product.PurchaseResult, fromTransactionUpdate: Bool = false @@ -144,8 +142,6 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { } } - /// - Throws: ``ErrorCode`` if the transaction fails to verify. - /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. func handle( transactionResult: TransactionResult, fromTransactionUpdate: Bool From 8c05b52663ad72575cc170eae720d948e962c300 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:28:19 +0100 Subject: [PATCH 09/31] docs --- Sources/Purchasing/Purchases/PurchasesType.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 0c5c65d124..d10f7c945c 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -1029,6 +1029,9 @@ public protocol PurchasesSwiftType: AnyObject { * - Throws: An error of type ``ErrorCode`` is thrown if a failure occurs while handling the purchase. * * - Returns: A ``StoreTransaction`` if there was a transacton found and handled for the provided product ID. + * + * - Important: This should only be used if you have enabled observer mode during SDK configuration using + * ``Configuration/Builder/with(observerMode:)`` */ @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) func handleObserverModeTransaction( From 2a9de8ae657d728a7913b3ac3eefc14dbadd47a2 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:39:26 +0100 Subject: [PATCH 10/31] remove unneeded tests --- Sources/Purchasing/Purchases/Purchases.swift | 2 +- .../StoreKit2TransactionListener.swift | 9 +-- .../StoreKit2TransactionListenerTests.swift | 76 +------------------ .../MockStoreKit2TransactionListener.swift | 18 ----- 4 files changed, 6 insertions(+), 99 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 6d9dfb63d2..57080a49c5 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1105,7 +1105,7 @@ public extension Purchases { func handleObserverModeTransaction( purchaseResult: StoreKit.Product.PurchaseResult ) async throws -> StoreTransaction? { - guard !self.systemInfo.observerMode else { + guard self.systemInfo.observerMode else { throw NewErrorUtils.observerModeNotEnabledError() } let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( diff --git a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift index c0b4045cd9..8b3419c265 100644 --- a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift +++ b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -44,13 +44,6 @@ protocol StoreKit2TransactionListenerType: Sendable { fromTransactionUpdate: Bool ) async throws -> StoreKit2TransactionListener.ResultData - /// - Throws: ``ErrorCode`` if the transaction fails to verify. - /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. - func handle( - transactionResult: StoreKit.VerificationResult, - fromTransactionUpdate: Bool - ) async throws -> StoreTransaction - } /// Observes `StoreKit.Transaction.updates`, which receives: @@ -142,6 +135,8 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { } } + /// - Throws: ``ErrorCode`` if the transaction fails to verify. + /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. func handle( transactionResult: TransactionResult, fromTransactionUpdate: Bool diff --git a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift index fd664e154b..b5b9a803d3 100644 --- a/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift +++ b/Tests/StoreKitUnitTests/StoreKit2/StoreKit2TransactionListenerTests.swift @@ -69,9 +69,9 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { expect(handle?.isCancelled) == true } - // MARK: - Purchase Result Tests + // MARK: - - func testVerifiedTransactoinInPurchaseResultReturnsOriginalTransaction() async throws { + func testVerifiedTransactionReturnsOriginalTransaction() async throws { try AvailabilityChecks.iOS15APIAvailableOrSkipTest() let fakeTransaction = try await self.simulateAnyPurchase() @@ -104,7 +104,7 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { } } - func testUnverifiedTransactionsInPurchaseResultReturnStoreProblemError() async throws { + func testUnverifiedTransactionsReturnStoreProblemError() async throws { try AvailabilityChecks.iOS15APIAvailableOrSkipTest() let transaction = try await self.simulateAnyPurchase() @@ -182,76 +182,6 @@ class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests { } } - // MARK: - Transaction Result Tests - - func testVerifiedTransactionReturnsOriginalTransaction() async throws { - try AvailabilityChecks.iOS15APIAvailableOrSkipTest() - - let fakeTransaction = try await self.simulateAnyPurchase() - - let transaction = try await self.listener.handle(transactionResult: fakeTransaction, - fromTransactionUpdate: false) - expect(transaction.sk2Transaction) == fakeTransaction.underlyingTransaction - } - - func testUnverifiedTransactionsReturnStoreProblemError() async throws { - try AvailabilityChecks.iOS15APIAvailableOrSkipTest() - - let transaction = try await self.simulateAnyPurchase() - let error: StoreKit.VerificationResult.VerificationError = .invalidSignature - let result: StoreKit.VerificationResult = .unverified(transaction.underlyingTransaction, error) - - // Note: can't use `expect().to(throwError)` or `XCTAssertThrowsError` - // because neither of them accept `async` - do { - _ = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: false) - XCTFail("Error expected") - } catch { - expect(error).to(matchError(ErrorCode.storeProblemError)) - } - } - - func testHandleTransactionDoesNotFinishTransaction() async throws { - let (_, result, _) = try await self.purchase() - - let resultData = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: false) - expect(resultData.sk2Transaction) == result.underlyingTransaction - - try await self.verifyUnfinishedTransaction(withId: result.underlyingTransaction.id) - } - - func testHandleTransactionResultDoesNotNotifyDelegate() async throws { - let result = try await self.purchase().verificationResult - _ = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: false) - - expect(self.delegate.invokedTransactionUpdated) == false - } - - func testHandleTransactoinResultNotifiesDelegate() async throws { - let result = try await self.purchase().verificationResult - _ = try await self.listener.handle(transactionResult: result, fromTransactionUpdate: true) - - expect(self.delegate.invokedTransactionUpdated) == true - } - - func testHandleUnverifiedTransaction() async throws { - let (_, _, transaction) = try await self.purchase() - - let verificationError: StoreKit.VerificationResult.VerificationError = .invalidSignature - - do { - _ = try await self.listener.handle( - transactionResult: .unverified(transaction, verificationError), fromTransactionUpdate: false - ) - fail("Expected error") - } catch { - expect(error).to(matchError(ErrorCode.storeProblemError)) - - let underlyingError = try XCTUnwrap((error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError) - expect(underlyingError).to(matchError(verificationError)) - } - } - } // MARK: - Transaction.updates tests diff --git a/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift b/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift index 3fe75b6c2e..d49a8d59fe 100644 --- a/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift +++ b/Tests/UnitTests/Mocks/MockStoreKit2TransactionListener.swift @@ -71,24 +71,6 @@ final class MockStoreKit2TransactionListener: StoreKit2TransactionListenerType { return (self.mockCancelled, transaction) } - - func handle( - transactionResult: StoreKit.VerificationResult, - fromTransactionUpdate: Bool = false - ) async throws -> StoreTransaction { - self.invokedHandle = true - self.invokedHandleCount += 1 - - let transaction: StoreTransaction = self.mockTransaction.value.map { - StoreTransaction(sk2Transaction: $0, - jwsRepresentation: self.mockJWSToken, - environmentOverride: self.mockEnvironment) - } ?? StoreTransaction(sk2Transaction: transactionResult.underlyingTransaction, - jwsRepresentation: self.mockJWSToken, - environmentOverride: self.mockEnvironment) - - return transaction - } } @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) From 1b7a1fecc7e9612b293d8238992b3aadd9901f8f Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:41:05 +0100 Subject: [PATCH 11/31] put back space --- Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index e3dae8729d..09e4d3a503 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -164,7 +164,7 @@ + (void)checkAPI { [p purchasePackage:pack withCompletion:^(RCStoreTransaction *t, RCCustomerInfo *i, NSError *e, BOOL userCancelled) { }]; [p restorePurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; [p syncPurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}]; - + [p checkTrialOrIntroDiscountEligibilityForProduct:storeProduct completion:^(RCIntroEligibilityStatus status) { }]; [p checkTrialOrIntroDiscountEligibility:@[@""] completion:^(NSDictionary *d) { }]; if (@available(iOS 12.2, macOS 10.14.4, macCatalyst 13.0, tvOS 12.2, watchOS 6.2, *)) { From e52b1d1766254a9eb56cf2d2a42a8bf3bef5d8bf Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:46:38 +0100 Subject: [PATCH 12/31] swiftify api --- Sources/Purchasing/Purchases/Purchases.swift | 4 ++-- Sources/Purchasing/Purchases/PurchasesType.swift | 4 ++-- .../SwiftAPITester/SwiftAPITester/PurchasesAPI.swift | 2 +- .../StoreKitObserverModeIntegrationTests.swift | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 57080a49c5..00a5627d4d 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1103,10 +1103,10 @@ public extension Purchases { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) func handleObserverModeTransaction( - purchaseResult: StoreKit.Product.PurchaseResult + _ purchaseResult: StoreKit.Product.PurchaseResult ) async throws -> StoreTransaction? { guard self.systemInfo.observerMode else { - throw NewErrorUtils.observerModeNotEnabledError() + throw NewErrorUtils.observerModeNotEnabledError().asPublicError } let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( purchaseResult: purchaseResult, fromTransactionUpdate: true) diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index d10f7c945c..e2f2e067bd 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -1019,7 +1019,7 @@ public protocol PurchasesSwiftType: AnyObject { * * ```swift * let result = try await product.purchase(options: options) - * Purchases.shared.handleObserverModeTransaction(purchaseResult: result) + * Purchases.shared.handleObserverModeTransaction(result) * ``` * * - Note: You need to finish the transaction yourself after calling this method. @@ -1035,7 +1035,7 @@ public protocol PurchasesSwiftType: AnyObject { */ @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) func handleObserverModeTransaction( - purchaseResult: StoreKit.Product.PurchaseResult + _ purchaseResult: StoreKit.Product.PurchaseResult ) async throws -> StoreTransaction? } diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 8ca44f9aea..8f924a4344 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -265,7 +265,7 @@ private func checkAsyncMethods(purchases: Purchases) async { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { let result = try await StoreKit.Product.products(for: [""]).first!.purchase() - let _: StoreTransaction? = try await purchases.handleObserverModeTransaction(purchaseResult: result) + let _: StoreTransaction? = try await purchases.handleObserverModeTransaction(result) } for try await _: CustomerInfo in purchases.customerInfoStream {} diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index 06e9959ff3..34e34215e6 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -51,7 +51,7 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) - _ = try await Purchases.shared.handleObserverModeTransaction(purchaseResult: result) + _ = try await Purchases.shared.handleObserverModeTransaction(result) try await asyncWait( description: "Entitlement didn't become active", @@ -99,7 +99,7 @@ class StoreKit2ObserverModeDisabledIntegrationTests: StoreKit1ObserverModeIntegr let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) do { - _ = try await Purchases.shared.handleObserverModeTransaction(purchaseResult: result) + _ = try await Purchases.shared.handleObserverModeTransaction(result) fail("Expected error") } catch { expect(error).to(matchError(ErrorCode.observerModeNotEnabledError)) From 48a84d98c0bd9169c4139769c9722ffbd5c5db38 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:46:47 +0100 Subject: [PATCH 13/31] fix test --- .../SwiftAPITester/ErrorCodesAPI.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift index 2671d53999..3f8122c3b9 100644 --- a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift +++ b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift @@ -53,7 +53,8 @@ func checkPurchasesErrorCodeEnums() { .invalidPromotionalOfferError, .offlineConnectionError, .featureNotAvailableInCustomEntitlementsComputationMode, - .signatureVerificationFailed: + .signatureVerificationFailed, + .observerModeNotEnabledError: print(errCode!) @unknown default: fatalError() From f0e4b229e7a41364540b8822892fd57f13aa9ba8 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 20:55:05 +0100 Subject: [PATCH 14/31] do not create new error --- Sources/Error Handling/ErrorCode.swift | 5 ----- Sources/Error Handling/ErrorUtils.swift | 17 +---------------- Sources/Logging/Strings/ConfigureStrings.swift | 4 ++++ Sources/Purchasing/Purchases/Purchases.swift | 4 +++- .../SwiftAPITester/ErrorCodesAPI.swift | 3 +-- .../SwiftAPITester/ErrorCodesAPI.swift | 3 +-- .../StoreKitObserverModeIntegrationTests.swift | 2 +- Tests/UnitTests/Purchasing/ErrorCodeTests.swift | 2 +- 8 files changed, 12 insertions(+), 28 deletions(-) diff --git a/Sources/Error Handling/ErrorCode.swift b/Sources/Error Handling/ErrorCode.swift index 8a31960201..920d39e7ab 100644 --- a/Sources/Error Handling/ErrorCode.swift +++ b/Sources/Error Handling/ErrorCode.swift @@ -60,7 +60,6 @@ import Foundation @objc(RCFeatureNotAvailableInCustomEntitlementsComputationMode) case featureNotAvailableInCustomEntitlementsComputationMode = 36 @objc(RCSignatureVerificationFailed) case signatureVerificationFailed = 37 - @objc(RCObserberModeNotEnabledError) case observerModeNotEnabledError = 38 // swiftlint:enable missing_docs @@ -181,8 +180,6 @@ extension ErrorCode: DescribableError { return "This feature is not available when utilizing the customEntitlementsComputation dangerousSetting." case .signatureVerificationFailed: return "Request failed signature verification. See https://rev.cat/trusted-entitlements for more info." - case .observerModeNotEnabledError: - return "Observer mode is not enabled. You must enable Observer Mode when initializing the SDKs." @unknown default: return "Something went wrong." @@ -284,8 +281,6 @@ extension ErrorCode { return "FEATURE_NOT_AVAILABLE_IN_CUSTOM_ENTITLEMENTS_COMPUTATION_MODE_ERROR" case .signatureVerificationFailed: return "SIGNATURE_VERIFICATION_FAILED" - case .observerModeNotEnabledError: - return "OBSERVER_MODE_NOT_ENABLED" @unknown default: return "UNRECOGNIZED_ERROR" } diff --git a/Sources/Error Handling/ErrorUtils.swift b/Sources/Error Handling/ErrorUtils.swift index 45d062ba01..dc8ab2ef19 100644 --- a/Sources/Error Handling/ErrorUtils.swift +++ b/Sources/Error Handling/ErrorUtils.swift @@ -78,20 +78,6 @@ enum ErrorUtils { ) } - /** - * Constructs an NSError with the ``ErrorCode/observerModeNotEnabledError`` code. - */ - static func observerModeNotEnabledError( - error: Error? = nil, - fileName: String = #fileID, functionName: String = #function, line: UInt = #line - ) -> PurchasesError { - let errorCode = ErrorCode.observerModeNotEnabledError - return ErrorUtils.error(with: errorCode, - message: errorCode.description, - underlyingError: error, - fileName: fileName, functionName: functionName, line: line) - } - /** * Maps a ``BackendErrorCode`` code to a ``ErrorCode``. code. Constructs an Error with the mapped code and adds a * `NSUnderlyingErrorKey` in the `NSError.userInfo` dictionary. The backend error code will be mapped using @@ -698,8 +684,7 @@ private extension ErrorUtils { .invalidPromotionalOfferError, .offlineConnectionError, .featureNotAvailableInCustomEntitlementsComputationMode, - .signatureVerificationFailed, - .observerModeNotEnabledError: + .signatureVerificationFailed: Logger.error( localizedDescription, fileName: fileName, diff --git a/Sources/Logging/Strings/ConfigureStrings.swift b/Sources/Logging/Strings/ConfigureStrings.swift index c3046f2900..933fef207d 100644 --- a/Sources/Logging/Strings/ConfigureStrings.swift +++ b/Sources/Logging/Strings/ConfigureStrings.swift @@ -73,6 +73,8 @@ enum ConfigureStrings { case sk2_required_for_swiftui_paywalls + case sk2_required_observer_mode + } extension ConfigureStrings: LogMessage { @@ -183,6 +185,8 @@ extension ConfigureStrings: LogMessage { return "Purchases is not configured with StoreKit 2 enabled. This is required in order to detect " + "transactions coming from SwiftUI paywalls. You must use `.with(storeKitVersion: .storeKit2)` " + "when configuring the SDK." + case .sk2_required_observer_mode: + return "Observer mode must be enabled. You must use `.with(observerMode: true)` when configuring the SDK." } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 00a5627d4d..003cdccfce 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1106,7 +1106,9 @@ public extension Purchases { _ purchaseResult: StoreKit.Product.PurchaseResult ) async throws -> StoreTransaction? { guard self.systemInfo.observerMode else { - throw NewErrorUtils.observerModeNotEnabledError().asPublicError + throw NewErrorUtils.configurationError( + message: Strings.configure.sk2_required_observer_mode.description + ).asPublicError } let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( purchaseResult: purchaseResult, fromTransactionUpdate: true) diff --git a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift index 3f8122c3b9..2671d53999 100644 --- a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift +++ b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift @@ -53,8 +53,7 @@ func checkPurchasesErrorCodeEnums() { .invalidPromotionalOfferError, .offlineConnectionError, .featureNotAvailableInCustomEntitlementsComputationMode, - .signatureVerificationFailed, - .observerModeNotEnabledError: + .signatureVerificationFailed: print(errCode!) @unknown default: fatalError() diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift index 38aa5cf441..3ec4270085 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/ErrorCodesAPI.swift @@ -53,8 +53,7 @@ func checkPurchasesErrorCodeEnums() { .invalidPromotionalOfferError, .offlineConnectionError, .featureNotAvailableInCustomEntitlementsComputationMode, - .signatureVerificationFailed, - .observerModeNotEnabledError: + .signatureVerificationFailed: print(errCode!) @unknown default: fatalError() diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index 34e34215e6..b2dd1d90fa 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -102,7 +102,7 @@ class StoreKit2ObserverModeDisabledIntegrationTests: StoreKit1ObserverModeIntegr _ = try await Purchases.shared.handleObserverModeTransaction(result) fail("Expected error") } catch { - expect(error).to(matchError(ErrorCode.observerModeNotEnabledError)) + expect(error).to(matchError(ErrorCode.configurationError)) } } diff --git a/Tests/UnitTests/Purchasing/ErrorCodeTests.swift b/Tests/UnitTests/Purchasing/ErrorCodeTests.swift index 26ecbb5926..2c68dac09f 100644 --- a/Tests/UnitTests/Purchasing/ErrorCodeTests.swift +++ b/Tests/UnitTests/Purchasing/ErrorCodeTests.swift @@ -179,7 +179,7 @@ class ErrorCodeTests: TestCase { } func testErrorCodeEnumCasesAreCoveredInTests() { - expect(ErrorCode.allCases).to(haveCount(38)) + expect(ErrorCode.allCases).to(haveCount(37)) } func ensureEnumCaseMatchesExpectedRawValue(errorCode: ErrorCode, expectedRawValue: Int) { From 373f081a740761f6f77d4464ffae108bf4e09555 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:01:59 +0100 Subject: [PATCH 15/31] remove leftover api --- .../ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m index 3d434b090e..568fbefaaa 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesErrorCodeAPI.m @@ -58,7 +58,6 @@ + (void)checkEnums { case RCInvalidPromotionalOfferError: case RCOfflineConnectionError: case RCSignatureVerificationFailed: - case RCObserberModeNotEnabledError: NSLog(@"%ld", (long)errCode); case RCFeatureNotAvailableInCustomEntitlementsComputationMode: break; From c800831e84de0e1ff74b7d5cba19505f67172489 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:02:23 +0100 Subject: [PATCH 16/31] thow error if using observer mode without storekit 2 --- Sources/Logging/Strings/ConfigureStrings.swift | 10 ++++++++-- Sources/Purchasing/Purchases/Purchases.swift | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Sources/Logging/Strings/ConfigureStrings.swift b/Sources/Logging/Strings/ConfigureStrings.swift index 933fef207d..1b5084ed59 100644 --- a/Sources/Logging/Strings/ConfigureStrings.swift +++ b/Sources/Logging/Strings/ConfigureStrings.swift @@ -73,7 +73,9 @@ enum ConfigureStrings { case sk2_required_for_swiftui_paywalls - case sk2_required_observer_mode + case observer_mode_required + + case sk2_required } @@ -185,8 +187,12 @@ extension ConfigureStrings: LogMessage { return "Purchases is not configured with StoreKit 2 enabled. This is required in order to detect " + "transactions coming from SwiftUI paywalls. You must use `.with(storeKitVersion: .storeKit2)` " + "when configuring the SDK." - case .sk2_required_observer_mode: + + case .observer_mode_required: return "Observer mode must be enabled. You must use `.with(observerMode: true)` when configuring the SDK." + + case .sk2_required: + return "StoreKit 2 must be enabled. You must use `.with(storeKitVersion: .storeKit2)` when configuring the SDK." } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 003cdccfce..7cb330a637 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1107,7 +1107,12 @@ public extension Purchases { ) async throws -> StoreTransaction? { guard self.systemInfo.observerMode else { throw NewErrorUtils.configurationError( - message: Strings.configure.sk2_required_observer_mode.description + message: Strings.configure.observer_mode_required.description + ).asPublicError + } + guard self.systemInfo.storeKitVersion == .storeKit2 else { + throw NewErrorUtils.configurationError( + message: Strings.configure.sk2_required.description ).asPublicError } let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( From 39a5a2253c1742bc7b76bb8b2659e2cc60158ad3 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:16:05 +0100 Subject: [PATCH 17/31] docs --- Sources/Purchasing/Purchases/Purchases.swift | 3 ++- .../Purchasing/Purchases/PurchasesType.swift | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 7cb330a637..353f9d4616 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1116,7 +1116,8 @@ public extension Purchases { ).asPublicError } let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( - purchaseResult: purchaseResult, fromTransactionUpdate: true) + purchaseResult: purchaseResult, fromTransactionUpdate: true + ) return transaction } diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index e2f2e067bd..1511d8919a 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -1018,11 +1018,32 @@ public protocol PurchasesSwiftType: AnyObject { * #### Example: * * ```swift - * let result = try await product.purchase(options: options) - * Purchases.shared.handleObserverModeTransaction(result) + * // Fetch and purchase the product + * let product = try await StoreKit.Product.products(for: ["my_product_id"]).first + * guard let product = product else { return } + * let result = try await product.purchase() + * // Let RevenueCat obserbe the transaction result + * _ = try await Purchases.shared.handleObserverModeTransaction(result) + * // Handle the result and finish the transaction + * switch result { + * case .success(let verification): + * switch verification { + * case .unverified(_, _): + * break + * case .verified(let transaction): + * // If the purchase was successful and verified, finish the transaction + * await transaction.finish() + * } + * case .userCancelled: + * break + * case .pending: + * break + * @unknown default: + * break + * } * ``` * - * - Note: You need to finish the transaction yourself after calling this method. + * - Warning: You need to finish the transaction yourself after calling this method. * * - Parameter purchaseResult: The ``StoreKit.Product.PurchaseResult`` of the product that was just purchased. * From 5d912822db4f2aee3cf6bd580919ce933de8e27a Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:17:08 +0100 Subject: [PATCH 18/31] docs --- Sources/Purchasing/Purchases/PurchasesType.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 1511d8919a..2b2da1518f 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -1045,7 +1045,7 @@ public protocol PurchasesSwiftType: AnyObject { * * - Warning: You need to finish the transaction yourself after calling this method. * - * - Parameter purchaseResult: The ``StoreKit.Product.PurchaseResult`` of the product that was just purchased. + * - Parameter purchaseResult: The `StoreKit.Product.PurchaseResult` of the product that was just purchased. * * - Throws: An error of type ``ErrorCode`` is thrown if a failure occurs while handling the purchase. * From 7686b634448223bed1b92fec42ae26135f72d229 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:19:22 +0100 Subject: [PATCH 19/31] strings --- Sources/Logging/Strings/ConfigureStrings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Logging/Strings/ConfigureStrings.swift b/Sources/Logging/Strings/ConfigureStrings.swift index 1b5084ed59..a85fc6b537 100644 --- a/Sources/Logging/Strings/ConfigureStrings.swift +++ b/Sources/Logging/Strings/ConfigureStrings.swift @@ -189,7 +189,7 @@ extension ConfigureStrings: LogMessage { "when configuring the SDK." case .observer_mode_required: - return "Observer mode must be enabled. You must use `.with(observerMode: true)` when configuring the SDK." + return "Attempted to manually handle transactions with observer mode not enabled. You must use `.with(observerMode: true)` when configuring the SDK." case .sk2_required: return "StoreKit 2 must be enabled. You must use `.with(storeKitVersion: .storeKit2)` when configuring the SDK." From baf90c6a5d1905c118949e4aa02f7a5c138bb581 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:32:36 +0100 Subject: [PATCH 20/31] tests --- .../StoreKitIntegrationTests.swift | 13 ++++++ ...StoreKitObserverModeIntegrationTests.swift | 46 ++++++++----------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift index 1604fab641..209eb20840 100644 --- a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift @@ -20,6 +20,19 @@ class StoreKit2IntegrationTests: StoreKit1IntegrationTests { override class var storeKitVersion: StoreKitVersion { .storeKit2 } + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testObservingTransactionThrowsIfObserverModeNotEnabled() async throws { + let manager = ObserverModeManager() + let result = try await manager.purchaseProductFromStoreKit2() + + do { + _ = try await Purchases.shared.handleObserverModeTransaction(result) + fail("Expected error") + } catch { + expect(error).to(matchError(ErrorCode.configurationError)) + } + } + } class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests { diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index b2dd1d90fa..d3e096de27 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -81,33 +81,6 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes } -class StoreKit2ObserverModeDisabledIntegrationTests: StoreKit1ObserverModeIntegrationTests { - - override class var storeKitVersion: StoreKitVersion { .storeKit2 } - - override class var observerMode: Bool { false } - - override func setUp() async throws { - try await super.setUp() - - try AvailabilityChecks.iOS15APIAvailableOrSkipTest() - } - - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) - func testObservingTransactionThrowsIfObserverModeNotEnabled() async throws { - let result = try await self.manager.purchaseProductFromStoreKit2() - let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) - - do { - _ = try await Purchases.shared.handleObserverModeTransaction(result) - fail("Expected error") - } catch { - expect(error).to(matchError(ErrorCode.configurationError)) - } - } - -} - class StoreKit1ObserverModeIntegrationTests: BaseStoreKitObserverModeIntegrationTests { override class var storeKitVersion: StoreKitVersion { .storeKit1 } @@ -211,3 +184,22 @@ class StoreKit1ObserverModeWithExistingPurchasesTests: BaseStoreKitObserverModeI } } + +class StoreKit2NotEnabledObserverModeIntegrationTests: BaseStoreKitObserverModeIntegrationTests { + + override class var storeKitVersion: StoreKitVersion { .storeKit1 } + + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testObservingTransactionThrowsIfStoreKit2NotEnabled() async throws { + let manager = ObserverModeManager() + let result = try await manager.purchaseProductFromStoreKit2() + + do { + _ = try await Purchases.shared.handleObserverModeTransaction(result) + fail("Expected error") + } catch { + expect(error).to(matchError(ErrorCode.configurationError)) + } + } + +} From 3754740548b16a6e881ae47cc741ef26b63c12a3 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:33:17 +0100 Subject: [PATCH 21/31] make sure error is an ErrorCode --- Sources/Purchasing/Purchases/Purchases.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 353f9d4616..805835699b 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1115,10 +1115,14 @@ public extension Purchases { message: Strings.configure.sk2_required.description ).asPublicError } - let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( - purchaseResult: purchaseResult, fromTransactionUpdate: true - ) - return transaction + do { + let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( + purchaseResult: purchaseResult, fromTransactionUpdate: true + ) + return transaction + } catch { + throw NewErrorUtils.purchasesError(withUntypedError: error).asPublicError + } } } From 9e58ecf38af6cfbd15a499e9a2d5c4e677c9d586 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 21:42:08 +0100 Subject: [PATCH 22/31] log message when storekit2 + observer mode --- Sources/Logging/Strings/ConfigureStrings.swift | 12 ++++++++++-- Sources/Purchasing/Configuration.swift | 7 +++++++ Tests/UnitTests/Purchasing/ConfigurationTests.swift | 11 +++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Sources/Logging/Strings/ConfigureStrings.swift b/Sources/Logging/Strings/ConfigureStrings.swift index a85fc6b537..ed4f97eaa2 100644 --- a/Sources/Logging/Strings/ConfigureStrings.swift +++ b/Sources/Logging/Strings/ConfigureStrings.swift @@ -77,6 +77,8 @@ enum ConfigureStrings { case sk2_required + case observer_mode_with_storekit2 + } extension ConfigureStrings: LogMessage { @@ -189,10 +191,16 @@ extension ConfigureStrings: LogMessage { "when configuring the SDK." case .observer_mode_required: - return "Attempted to manually handle transactions with observer mode not enabled. You must use `.with(observerMode: true)` when configuring the SDK." + return "Attempted to manually handle transactions with observer mode not enabled. " + + "You must use `.with(observerMode: true)` when configuring the SDK." case .sk2_required: - return "StoreKit 2 must be enabled. You must use `.with(storeKitVersion: .storeKit2)` when configuring the SDK." + return "StoreKit 2 must be enabled. You must use `.with(storeKitVersion: .storeKit2)` " + + "when configuring the SDK." + + case .observer_mode_with_storekit2: + return "StoreKit 2 Observer Mode is enabled. You must manually handle newly purchased transactions " + + "calling `Purchases.shared.handleObserverModeTransaction()`." } } diff --git a/Sources/Purchasing/Configuration.swift b/Sources/Purchasing/Configuration.swift index 10a2dbbcb8..d691847aff 100644 --- a/Sources/Purchasing/Configuration.swift +++ b/Sources/Purchasing/Configuration.swift @@ -54,6 +54,7 @@ import Foundation private init(with builder: Builder) { Self.verify(apiKey: builder.apiKey) + Self.verify(observerMode: builder.observerMode, storeKitVersion: builder.storeKitVersion) self.apiKey = builder.apiKey self.appUserID = builder.appUserID @@ -323,6 +324,12 @@ extension Configuration { } } + fileprivate static func verify(observerMode: Bool, storeKitVersion: StoreKitVersion) { + if observerMode, storeKitVersion == .storeKit2 { + Logger.warn(Strings.configure.observer_mode_with_storekit2) + } + } + private static let applePlatformKeyPrefix: String = "appl_" } diff --git a/Tests/UnitTests/Purchasing/ConfigurationTests.swift b/Tests/UnitTests/Purchasing/ConfigurationTests.swift index e5b5fc5e2c..94b9c0a672 100644 --- a/Tests/UnitTests/Purchasing/ConfigurationTests.swift +++ b/Tests/UnitTests/Purchasing/ConfigurationTests.swift @@ -56,4 +56,15 @@ class ConfigurationTests: TestCase { expect(configuration.storeKitVersion) == .default } + func testObserverModeWithStoreKit2() { + let configuration = Configuration.Builder(withAPIKey: "test") + .with(observerMode: true) + .build() + + expect(configuration.observerMode) == true + expect(configuration.storeKitVersion) == .storeKit2 + + self.logger.verifyMessageWasLogged(Strings.configure.observer_mode_with_storekit2) + } + } From 31d1d5c599b08d2269c643c4ce71eb4ed0df26d8 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 23:57:46 +0100 Subject: [PATCH 23/31] Remove StoreKit2NotEnabledObserverModeIntegrationTests from offline/other/sk1 tests --- .../BackendIntegrationTests-Offline.xctestplan | 1 + BackendIntegrationTests/BackendIntegrationTests-Other.xctestplan | 1 + BackendIntegrationTests/BackendIntegrationTests-SK1.xctestplan | 1 + 3 files changed, 3 insertions(+) diff --git a/BackendIntegrationTests/BackendIntegrationTests-Offline.xctestplan b/BackendIntegrationTests/BackendIntegrationTests-Offline.xctestplan index 7b2a0a7606..e8b081351e 100644 --- a/BackendIntegrationTests/BackendIntegrationTests-Offline.xctestplan +++ b/BackendIntegrationTests/BackendIntegrationTests-Offline.xctestplan @@ -60,6 +60,7 @@ "StoreKit2JWSIntegrationTests", "StoreKit2JWSObserverModeIntegrationTests", "StoreKit2JWSObserverModeWithExistingPurchasesTests", + "StoreKit2NotEnabledObserverModeIntegrationTests", "StoreKit2ObserverModeIntegrationTests", "StoreKit2ObserverModeWithExistingPurchasesTests", "SubscriberAttributesManagerIntegrationTests", diff --git a/BackendIntegrationTests/BackendIntegrationTests-Other.xctestplan b/BackendIntegrationTests/BackendIntegrationTests-Other.xctestplan index c3eaa25ef7..b923468622 100644 --- a/BackendIntegrationTests/BackendIntegrationTests-Other.xctestplan +++ b/BackendIntegrationTests/BackendIntegrationTests-Other.xctestplan @@ -54,6 +54,7 @@ "StoreKit2JWSIntegrationTests", "StoreKit2JWSObserverModeIntegrationTests", "StoreKit2JWSObserverModeWithExistingPurchasesTests", + "StoreKit2NotEnabledObserverModeIntegrationTests", "StoreKit2ObserverModeIntegrationTests", "StoreKit2ObserverModeWithExistingPurchasesTests", "TestCase" diff --git a/BackendIntegrationTests/BackendIntegrationTests-SK1.xctestplan b/BackendIntegrationTests/BackendIntegrationTests-SK1.xctestplan index d00e33d148..965e8977b7 100644 --- a/BackendIntegrationTests/BackendIntegrationTests-SK1.xctestplan +++ b/BackendIntegrationTests/BackendIntegrationTests-SK1.xctestplan @@ -61,6 +61,7 @@ "StoreKit2JWSIntegrationTests", "StoreKit2JWSObserverModeIntegrationTests", "StoreKit2JWSObserverModeWithExistingPurchasesTests", + "StoreKit2NotEnabledObserverModeIntegrationTests", "StoreKit2ObserverModeIntegrationTests", "StoreKit2ObserverModeWithExistingPurchasesTests", "SubscriberAttributesManagerIntegrationTests", From e6008c33bd36ba518ea7a7c8eb5106fe90bdb81c Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 23:58:44 +0100 Subject: [PATCH 24/31] documentation and error messages --- Sources/Logging/Strings/ConfigureStrings.swift | 4 ++-- Sources/Logging/Strings/PurchaseStrings.swift | 2 +- Sources/Purchasing/Purchases/Purchases.swift | 2 +- Sources/Purchasing/Purchases/PurchasesType.swift | 2 +- .../Purchasing/StoreKit2/StoreKit2TransactionListener.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Logging/Strings/ConfigureStrings.swift b/Sources/Logging/Strings/ConfigureStrings.swift index ed4f97eaa2..2d50e81c3e 100644 --- a/Sources/Logging/Strings/ConfigureStrings.swift +++ b/Sources/Logging/Strings/ConfigureStrings.swift @@ -73,7 +73,7 @@ enum ConfigureStrings { case sk2_required_for_swiftui_paywalls - case observer_mode_required + case handle_transaction_observer_mode_required case sk2_required @@ -190,7 +190,7 @@ extension ConfigureStrings: LogMessage { "transactions coming from SwiftUI paywalls. You must use `.with(storeKitVersion: .storeKit2)` " + "when configuring the SDK." - case .observer_mode_required: + case .handle_transaction_observer_mode_required: return "Attempted to manually handle transactions with observer mode not enabled. " + "You must use `.with(observerMode: true)` when configuring the SDK." diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index 36fcf211ac..3df35f19bd 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -86,7 +86,7 @@ enum PurchaseStrings { case caching_presented_offering_identifier(offeringID: String, productID: String) case payment_queue_wrapper_delegate_call_sk1_enabled case restorepurchases_called_with_allow_sharing_appstore_account_false - case sk2_observer_mode_error_processing_transaction(error: Error) + case sk2_observer_mode_error_processing_transaction(Error) } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 805835699b..ed6da4408e 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1107,7 +1107,7 @@ public extension Purchases { ) async throws -> StoreTransaction? { guard self.systemInfo.observerMode else { throw NewErrorUtils.configurationError( - message: Strings.configure.observer_mode_required.description + message: Strings.configure.handle_transaction_observer_mode_required.description ).asPublicError } guard self.systemInfo.storeKitVersion == .storeKit2 else { diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 2b2da1518f..c7b27a88ed 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -1022,7 +1022,7 @@ public protocol PurchasesSwiftType: AnyObject { * let product = try await StoreKit.Product.products(for: ["my_product_id"]).first * guard let product = product else { return } * let result = try await product.purchase() - * // Let RevenueCat obserbe the transaction result + * // Let RevenueCat handle the transaction result * _ = try await Purchases.shared.handleObserverModeTransaction(result) * // Handle the result and finish the transaction * switch result { diff --git a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift index 8b3419c265..6066ebf0b0 100644 --- a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift +++ b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -37,7 +37,7 @@ protocol StoreKit2TransactionListenerType: Sendable { func set(delegate: StoreKit2TransactionListenerDelegate) async - /// - Throws: ``ErrorCode`` if purchase was not completed successfully + /// - Throws: ``PurchasesError`` if purchase was not completed successfully /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. func handle( purchaseResult: StoreKit.Product.PurchaseResult, From 7b34817af47ea696fd528937310b6b9604c56a8a Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 23:58:54 +0100 Subject: [PATCH 25/31] fix build --- Tests/UnitTests/Mocks/MockPurchases.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/UnitTests/Mocks/MockPurchases.swift b/Tests/UnitTests/Mocks/MockPurchases.swift index f60c4234a9..c9258784f9 100644 --- a/Tests/UnitTests/Mocks/MockPurchases.swift +++ b/Tests/UnitTests/Mocks/MockPurchases.swift @@ -460,7 +460,7 @@ extension MockPurchases: PurchasesSwiftType { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) func handleObserverModeTransaction( - purchaseResult: Product.PurchaseResult + _ purchaseResult: Product.PurchaseResult ) async throws -> RevenueCat.StoreTransaction? { self.unimplemented() } From f7d0a1f96c48b0297dcbe0e8de816ce69d0f1e03 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 17 Jan 2024 23:59:08 +0100 Subject: [PATCH 26/31] simplify test --- .../StoreKitObserverModeIntegrationTests.swift | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index d3e096de27..73b7f86ef2 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -53,17 +53,8 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes _ = try await Purchases.shared.handleObserverModeTransaction(result) - try await asyncWait( - description: "Entitlement didn't become active", - timeout: .seconds(5), - pollInterval: .milliseconds(500) - ) { - let entitlement = await self.purchasesDelegate - .customerInfo? - .entitlements[Self.entitlementIdentifier] - - return entitlement?.isActive == true - } + let customerInfo = try XCTUnwrap(self.purchasesDelegate.customerInfo) + try await self.verifyEntitlementWentThrough(customerInfo) } @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) From 3fc2dacd9036e270d5eda796e05d4914e16b354b Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Thu, 18 Jan 2024 00:03:01 +0100 Subject: [PATCH 27/31] make extension private again --- .../Purchasing/StoreKit2/StoreKit2TransactionListener.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift index 6066ebf0b0..a86dbf5b12 100644 --- a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift +++ b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -134,6 +134,10 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { ) } } +} + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +private extension StoreKit2TransactionListener { /// - Throws: ``ErrorCode`` if the transaction fails to verify. /// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. From b00ef6ab872e683a92ee1c6c90371d2f32f7998f Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Thu, 18 Jan 2024 00:03:13 +0100 Subject: [PATCH 28/31] fix test --- Tests/UnitTests/Purchasing/ConfigurationTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/UnitTests/Purchasing/ConfigurationTests.swift b/Tests/UnitTests/Purchasing/ConfigurationTests.swift index 94b9c0a672..bec691aaa7 100644 --- a/Tests/UnitTests/Purchasing/ConfigurationTests.swift +++ b/Tests/UnitTests/Purchasing/ConfigurationTests.swift @@ -59,6 +59,7 @@ class ConfigurationTests: TestCase { func testObserverModeWithStoreKit2() { let configuration = Configuration.Builder(withAPIKey: "test") .with(observerMode: true) + .with(storeKitVersion: .storeKit2) .build() expect(configuration.observerMode) == true From eb83202b1d86ded6900033e6410987913459bf80 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Thu, 18 Jan 2024 00:07:19 +0100 Subject: [PATCH 29/31] space --- Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift index a86dbf5b12..f5c08cf5e6 100644 --- a/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift +++ b/Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift @@ -134,6 +134,7 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { ) } } + } @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) From 8e68c20c63a060e5947761fd2caf4394b97e470f Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Thu, 18 Jan 2024 11:35:11 +0100 Subject: [PATCH 30/31] reset tests --- .../Purchasing/ConfigurationTests.swift | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Tests/UnitTests/Purchasing/ConfigurationTests.swift b/Tests/UnitTests/Purchasing/ConfigurationTests.swift index bec691aaa7..7d0d522d5b 100644 --- a/Tests/UnitTests/Purchasing/ConfigurationTests.swift +++ b/Tests/UnitTests/Purchasing/ConfigurationTests.swift @@ -31,29 +31,35 @@ class ConfigurationTests: TestCase { expect(Configuration.validate(apiKey: "swRTCezdEzjnJSxdexDNJfcfiFrMXwqZ")) == .legacy } - func testStoreKitVersionUsesStoreKit1ByDefault() { - let configuration = Configuration.Builder(withAPIKey: "test") - .build() + func testNoObserverModeWithStoreKit1() { + let configuration = Configuration.Builder(withAPIKey: "test").build() - expect(configuration.storeKitVersion) == .default + expect(configuration.observerMode) == false + expect(configuration.storeKitVersion) == .storeKit1 + + self.logger.verifyMessageWasNotLogged(Strings.configure.observer_mode_with_storekit2) } - @available(*, deprecated) - func testLegacyFlagSetsStoreKitVersionWhenStoreKit2Enabled() { + func testNoObserverModeWithStoreKit2() { let configuration = Configuration.Builder(withAPIKey: "test") - .with(usesStoreKit2IfAvailable: true) + .with(storeKitVersion: .storeKit2) .build() + expect(configuration.observerMode) == false expect(configuration.storeKitVersion) == .storeKit2 + + self.logger.verifyMessageWasNotLogged(Strings.configure.observer_mode_with_storekit2) } - @available(*, deprecated) - func testLegacyFlagSetsStoreKitVersionWhenStoreKit1Enabled() { + func testObserverModeWithStoreKit1() { let configuration = Configuration.Builder(withAPIKey: "test") - .with(usesStoreKit2IfAvailable: false) + .with(observerMode: true) .build() - expect(configuration.storeKitVersion) == .default + expect(configuration.observerMode) == true + expect(configuration.storeKitVersion) == .storeKit1 + + self.logger.verifyMessageWasNotLogged(Strings.configure.observer_mode_with_storekit2) } func testObserverModeWithStoreKit2() { @@ -65,7 +71,8 @@ class ConfigurationTests: TestCase { expect(configuration.observerMode) == true expect(configuration.storeKitVersion) == .storeKit2 - self.logger.verifyMessageWasLogged(Strings.configure.observer_mode_with_storekit2) + self.logger.verifyMessageWasLogged(Strings.configure.observer_mode_with_storekit2, + level: .warn) } } From 97bd1152c22591c526548a3960b55068c7c00097 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Thu, 18 Jan 2024 11:36:53 +0100 Subject: [PATCH 31/31] add tests --- .../Purchasing/ConfigurationTests.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Tests/UnitTests/Purchasing/ConfigurationTests.swift b/Tests/UnitTests/Purchasing/ConfigurationTests.swift index 7d0d522d5b..c7c25e84e9 100644 --- a/Tests/UnitTests/Purchasing/ConfigurationTests.swift +++ b/Tests/UnitTests/Purchasing/ConfigurationTests.swift @@ -75,4 +75,29 @@ class ConfigurationTests: TestCase { level: .warn) } + func testStoreKitVersionUsesStoreKit1ByDefault() { + let configuration = Configuration.Builder(withAPIKey: "test") + .build() + + expect(configuration.storeKitVersion) == .default + } + + @available(*, deprecated) + func testLegacyFlagSetsStoreKitVersionWhenStoreKit2Enabled() { + let configuration = Configuration.Builder(withAPIKey: "test") + .with(usesStoreKit2IfAvailable: true) + .build() + + expect(configuration.storeKitVersion) == .storeKit2 + } + + @available(*, deprecated) + func testLegacyFlagSetsStoreKitVersionWhenStoreKit1Enabled() { + let configuration = Configuration.Builder(withAPIKey: "test") + .with(usesStoreKit2IfAvailable: false) + .build() + + expect(configuration.storeKitVersion) == .default + } + }