From d4b5d98f0d88e827fca568f5e10bc1f193aee2d0 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 7 Oct 2022 11:22:48 -0700 Subject: [PATCH] Fixed --- Sources/Logging/Strings/PurchaseStrings.swift | 9 ++--- Sources/Misc/Deprecations.swift | 4 +- .../Purchases/PurchasesOrchestrator.swift | 36 +++++++++++++---- .../SK1StoreTransaction.swift | 3 +- .../SK2StoreTransaction.swift | 4 +- .../StoreTransaction.swift | 6 +-- .../StoreKitIntegrationTests.swift | 13 ++++++- .../Mocks/MockStoreKit1Wrapper.swift | 4 +- .../Purchases/PurchasesPurchasingTests.swift | 39 ++++++++++++++----- 9 files changed, 85 insertions(+), 33 deletions(-) diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index 7c6f61a791..8a74d21c89 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -20,7 +20,7 @@ enum PurchaseStrings { case cannot_purchase_product_appstore_configuration_error case entitlements_revoked_syncing_purchases(productIdentifiers: [String]) - case finishing_transaction(transaction: SKPaymentTransaction) + case finishing_transaction(StoreTransaction) case purchasing_with_observer_mode_and_finish_transactions_false_warning case paymentqueue_removedtransaction(transaction: SKPaymentTransaction) case paymentqueue_revoked_entitlements_for_product_identifiers(productIdentifiers: [String]) @@ -80,10 +80,9 @@ extension PurchaseStrings: CustomStringConvertible { return "Entitlements revoked for product " + "identifiers: \(productIdentifiers). \nsyncing purchases" - case .finishing_transaction(let transaction): - return "Finishing transaction \(transaction.payment.productIdentifier) " + - "\(transaction.transactionIdentifier ?? "") " + - "(\(transaction.original?.transactionIdentifier ?? ""))" + case let .finishing_transaction(transaction): + return "Finishing transaction '\(transaction.transactionIdentifier)' " + + "for product '\(transaction.productIdentifier)'" case .purchasing_with_observer_mode_and_finish_transactions_false_warning: return "Observer mode is active (finishTransactions is set to false) and " + diff --git a/Sources/Misc/Deprecations.swift b/Sources/Misc/Deprecations.swift index 4c9ab6473e..d1bfb7e77d 100644 --- a/Sources/Misc/Deprecations.swift +++ b/Sources/Misc/Deprecations.swift @@ -403,8 +403,8 @@ extension CustomerInfo { self.quantity = 1 } - func finish(_ wrapper: PaymentQueueWrapperType) { - // Nothing to do + func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping () -> Void) { + completion() } } diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index e332a37414..7b2c592a71 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -726,7 +726,7 @@ private extension PurchasesOrchestrator { } } - self.finishTransactionIfNeeded(storeTransaction) + self.finishTransactionIfNeeded(storeTransaction, completion: {}) } func handleDeferredTransaction(_ transaction: SKPaymentTransaction) { @@ -875,23 +875,30 @@ private extension PurchasesOrchestrator { error: result.error) let completion = self.getAndRemovePurchaseCompletedCallback(forTransaction: transaction) + let error = result.error let finishable = error?.finishable ?? false switch result { case let .success(customerInfo): self.customerInfoManager.cache(customerInfo: customerInfo, appUserID: appUserID) - completion?(transaction, customerInfo, nil, false) - self.finishTransactionIfNeeded(transaction) + self.finishTransactionIfNeeded(transaction) { + completion?(transaction, customerInfo, nil, false) + } case let .failure(error): let purchasesError = error.asPublicError - completion?(transaction, nil, purchasesError, false) + @MainActor + func complete() { + completion?(transaction, nil, purchasesError, false) + } if finishable { - self.finishTransactionIfNeeded(transaction) + self.finishTransactionIfNeeded(transaction) { complete() } + } else { + complete() } } } @@ -1018,10 +1025,23 @@ private extension PurchasesOrchestrator { self.offeringsManager.invalidateAndReFetchCachedOfferingsIfAppropiate(appUserID: self.appUserID) } - func finishTransactionIfNeeded(_ transaction: StoreTransaction) { - if self.finishTransactions { - transaction.finish(self.paymentQueueWrapper.paymentQueueWrapperType) + func finishTransactionIfNeeded( + _ transaction: StoreTransaction, + completion: @escaping @Sendable @MainActor () -> Void + ) { + @Sendable + func complete() { + self.operationDispatcher.dispatchOnMainActor(completion) } + + guard self.finishTransactions else { + complete() + return + } + + Logger.purchase(Strings.purchase.finishing_transaction(transaction)) + + transaction.finish(self.paymentQueueWrapper.paymentQueueWrapperType, completion: complete) } } diff --git a/Sources/Purchasing/StoreKitAbstractions/SK1StoreTransaction.swift b/Sources/Purchasing/StoreKitAbstractions/SK1StoreTransaction.swift index 862a90de84..60ecbf4b77 100644 --- a/Sources/Purchasing/StoreKitAbstractions/SK1StoreTransaction.swift +++ b/Sources/Purchasing/StoreKitAbstractions/SK1StoreTransaction.swift @@ -31,8 +31,9 @@ internal struct SK1StoreTransaction: StoreTransactionType { let transactionIdentifier: String let quantity: Int - func finish(_ wrapper: PaymentQueueWrapperType) { + func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping () -> Void) { wrapper.finishTransaction(self.underlyingSK1Transaction) + completion() } } diff --git a/Sources/Purchasing/StoreKitAbstractions/SK2StoreTransaction.swift b/Sources/Purchasing/StoreKitAbstractions/SK2StoreTransaction.swift index f97a2d2ba4..209a9927ea 100644 --- a/Sources/Purchasing/StoreKitAbstractions/SK2StoreTransaction.swift +++ b/Sources/Purchasing/StoreKitAbstractions/SK2StoreTransaction.swift @@ -32,8 +32,8 @@ internal struct SK2StoreTransaction: StoreTransactionType { let transactionIdentifier: String let quantity: Int - func finish(_ wrapper: PaymentQueueWrapperType) { - _ = Task { + func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping () -> Void) { + Async.call(with: completion) { await self.underlyingSK2Transaction.finish() } } diff --git a/Sources/Purchasing/StoreKitAbstractions/StoreTransaction.swift b/Sources/Purchasing/StoreKitAbstractions/StoreTransaction.swift index 69b1c7dd53..62f65f41a3 100644 --- a/Sources/Purchasing/StoreKitAbstractions/StoreTransaction.swift +++ b/Sources/Purchasing/StoreKitAbstractions/StoreTransaction.swift @@ -42,8 +42,8 @@ public typealias SK2Transaction = StoreKit.Transaction @objc public var transactionIdentifier: String { self.transaction.transactionIdentifier } @objc public var quantity: Int { self.transaction.quantity } - func finish(_ wrapper: PaymentQueueWrapperType) { - self.transaction.finish(wrapper) + func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void) { + self.transaction.finish(wrapper, completion: completion) } // swiftlint:enable missing_docs @@ -87,7 +87,7 @@ internal protocol StoreTransactionType: Sendable { /// Indicates to the App Store that the app delivered the purchased content /// or enabled the service to finish the transaction. - func finish(_ wrapper: PaymentQueueWrapperType) + func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void) } diff --git a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift index 0864db3263..0d99922776 100644 --- a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift @@ -75,6 +75,9 @@ class StoreKit1IntegrationTests: BaseBackendIntegrationTests { } func testCanPurchaseConsumableMultipleTimes() async throws { + // See https://revenuecats.atlassian.net/browse/TRIAGE-134 + try XCTSkipIf(Self.storeKit2Setting == .disabled, "This test is not currently passing on SK1") + let count = 2 for _ in 0.. PurchaseResultData { - let offering = try await XCTAsyncUnwrap(try await Purchases.shared.offerings().offering(identifier: "coins")) - let package = try XCTUnwrap(offering.package(identifier: "10.coins")) + let offering = try await XCTAsyncUnwrap( + try await Purchases.shared.offerings().offering(identifier: "coins"), + file: file, line: line + ) + let package = try XCTUnwrap( + offering.package(identifier: "10.coins"), + file: file, line: line + ) return try await Purchases.shared.purchase(package: package) } diff --git a/Tests/UnitTests/Mocks/MockStoreKit1Wrapper.swift b/Tests/UnitTests/Mocks/MockStoreKit1Wrapper.swift index b970d96aeb..69bffa1762 100644 --- a/Tests/UnitTests/Mocks/MockStoreKit1Wrapper.swift +++ b/Tests/UnitTests/Mocks/MockStoreKit1Wrapper.swift @@ -26,9 +26,11 @@ class MockStoreKit1Wrapper: StoreKit1Wrapper { } var finishCalled = false + var finishProductIdentifier: String? override func finishTransaction(_ transaction: SKPaymentTransaction) { - finishCalled = true + self.finishCalled = true + self.finishProductIdentifier = transaction.productIdentifier } weak var mockDelegate: StoreKit1WrapperDelegate? diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift index 37e545be92..7551ae5d3d 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift @@ -98,13 +98,23 @@ class PurchasesPurchasingTests: BasePurchasesTests { } func testFinishesTransactionsIfSentToBackendCorrectly() throws { - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in } + var finished = true + + let productID = "com.product.id1" + let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: productID)) + + self.purchases.purchase(product: product) { (_, _, _, _) in + // Transactions must be finished by the time the callback is invoked. + expect(self.storeKit1Wrapper.finishCalled) == true + expect(self.storeKit1Wrapper.finishProductIdentifier) == productID + + finished = true + } let transaction = MockTransaction() transaction.mockPayment = try XCTUnwrap(self.storeKit1Wrapper.payment) + transaction.mockState = .purchasing - transaction.mockState = SKPaymentTransactionState.purchasing self.storeKit1Wrapper.delegate?.storeKit1Wrapper(self.storeKit1Wrapper, updatedTransaction: transaction) self.backend.postReceiptResult = .success(try CustomerInfo(data: Self.emptyCustomerInfoData)) @@ -113,7 +123,7 @@ class PurchasesPurchasingTests: BasePurchasesTests { self.storeKit1Wrapper.delegate?.storeKit1Wrapper(self.storeKit1Wrapper, updatedTransaction: transaction) expect(self.backend.postReceiptDataCalled) == true - expect(self.storeKit1Wrapper.finishCalled).toEventually(beTrue()) + expect(finished).toEventually(beTrue()) } func testDoesntFinishTransactionsIfFinishingDisabled() throws { @@ -157,11 +167,22 @@ class PurchasesPurchasingTests: BasePurchasesTests { } func testAfterSendingFinishesFromBackendErrorIfAppropriate() throws { - let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) - self.purchases.purchase(product: product) { (_, _, _, _) in } + var finished = false + + let productID = "com.product.id1" + let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: productID)) + + self.purchases.purchase(product: product) { (_, _, _, _) in + // Transactions must be finished by the time the callback is invoked. + expect(self.storeKit1Wrapper.finishCalled) == true + expect(self.storeKit1Wrapper.finishProductIdentifier) == productID + + finished = true + } let transaction = MockTransaction() transaction.mockPayment = try XCTUnwrap(self.storeKit1Wrapper.payment) + transaction.mockState = .purchased self.backend.postReceiptResult = .failure( .networkError(.errorResponse( @@ -170,11 +191,10 @@ class PurchasesPurchasingTests: BasePurchasesTests { )) ) - transaction.mockState = SKPaymentTransactionState.purchased self.storeKit1Wrapper.delegate?.storeKit1Wrapper(self.storeKit1Wrapper, updatedTransaction: transaction) expect(self.backend.postReceiptDataCalled) == true - expect(self.storeKit1Wrapper.finishCalled).toEventually(beTrue()) + expect(finished).toEventually(beTrue()) } func testNotifiesIfTransactionFailsFromBackend() throws { @@ -525,10 +545,11 @@ class PurchasesPurchasingTests: BasePurchasesTests { receivedError = error as NSError? secondCompletionCalled = true } + + self.performTransaction() } self.performTransaction() - self.performTransaction() } expect(secondCompletionCalled).toEventually(beTrue(), timeout: .seconds(10))