From 70365df383180d0225a8ff3df05444275bad3c4e Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 11 Oct 2022 13:38:54 -0700 Subject: [PATCH] Purchasing: fixed consumable purchases by fixing transaction-finishing (#1965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1964, [TRIAGE-134], [TRIAGE-131], and possibly [TRIAGE-82]. Depends on #1967, #1968. ### Fixes: - For `SK2` purchases we were never finishing transactions. We are now. - For `SK1` purchases, transactions were finished _after_ the completion block was invoked (and tests were very lenient checking that _eventually_ this happened). This could have lead to race conditions. - For SK2 only (at least in `SKTestSession`s), this fixes the ability to purchase multiple consumable purchases. This is the log from the failing test using SK2:
Open ``` 2022-10-06 15:18:52.286813-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:6f630d50b8294288a2d0862629d766fb/offerings 2022-10-06 15:18:52.383594-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ API request completed: GET /v1/subscribers/$RCAnonymousID:6f630d50b8294288a2d0862629d766fb/offerings 200 2022-10-06 15:18:52.388247-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ No existing products cached, starting store products request for: ["com.revenuecat.weekly_1.99.3_day_intro", "consumable.10_coins", "com.revenuecat.annual_39.99.2_week_intro", "com.revenuecat.monthly_4.99.1_week_intro"] 2022-10-06 15:18:52.388382-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Found an existing request for products: ["com.revenuecat.monthly_4.99.1_week_intro", "com.revenuecat.annual_39.99.2_week_intro", "com.revenuecat.weekly_1.99.3_day_intro", "consumable.10_coins"], appending to completion 2022-10-06 15:18:52.388451-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ GetOfferingsOperation: Finished 2022-10-06 15:18:52.388560-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Serial request done: GET subscribers/$RCAnonymousID%3A6f630d50b8294288a2d0862629d766fb/offerings, 0 requests left in the queue 2022-10-06 15:18:52.420210-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: 😻 Store products request request received response 2022-10-06 15:18:52.420358-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Store products request finished 2022-10-06 15:18:52.421126-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ Vending Offerings from cache 2022-10-06 15:18:52.421353-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins' 2022-10-06 15:18:52.496438-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple. 2022-10-06 15:18:52.510974-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/A3576DC2-355E-45BA-B32C-D2C0A3811BB4/data/Containers/Data/Application/BFD385C4-52BC-414B-9C1C-44522ED1222F/StoreKit/receipt 2022-10-06 15:18:52.511072-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-06 15:18:52.511181-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-06 15:18:52.511564-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:6f630d50b8294288a2d0862629d766fb 2022-10-06 15:18:52.515448-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started 2022-10-06 15:18:52.516403-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - INFO: ℹ️ Receipt parsed successfully 2022-10-06 15:18:52.516763-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: { "opaque_value" : "oX91ewAAAAA=", "sha1_hash" : "GsyNpUKQ89SonYyj\/jpw8\/N5nr8=", "bundle_id" : "com.revenuecat.StoreKitTestApp", "in_app_purchases" : [ { "quantity" : 1, "product_id" : "consumable.10_coins", "purchase_date" : "2022-10-06T22:18:52Z", "transaction_id" : "0" } ], "application_version" : "1", "creation_date" : "2022-10-06T22:18:52Z", "expiration_date" : "4001-01-01T00:00:00Z" } 2022-10-06 15:18:52.516825-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts 2022-10-06 15:18:52.518059-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts 2022-10-06 15:18:52.960048-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200 2022-10-06 15:18:52.964274-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished 2022-10-06 15:18:52.964666-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue 2022-10-06 15:18:52.966476-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Sending updated CustomerInfo to delegate. 2022-10-06 15:18:52.966697-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins' 2022-10-06 15:18:52.966973-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ Vending Offerings from cache 2022-10-06 15:18:52.967284-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins' 2022-10-06 15:18:52.991334-0700 BackendIntegrationTestsHostApp[45464:7076973] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple. 2022-10-06 15:18:53.015046-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/A3576DC2-355E-45BA-B32C-D2C0A3811BB4/data/Containers/Data/Application/BFD385C4-52BC-414B-9C1C-44522ED1222F/StoreKit/receipt 2022-10-06 15:18:53.015174-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-06 15:18:53.015262-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-06 15:18:53.015419-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:6f630d50b8294288a2d0862629d766fb 2022-10-06 15:18:53.015927-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started 2022-10-06 15:18:53.016558-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - INFO: ℹ️ Receipt parsed successfully 2022-10-06 15:18:53.016727-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: { "opaque_value" : "LP9LZQwAAAA=", "sha1_hash" : "WdANBTWr7wYWh8RYEgmGebO8DR8=", "bundle_id" : "com.revenuecat.StoreKitTestApp", "in_app_purchases" : [ { "quantity" : 1, "product_id" : "consumable.10_coins", "purchase_date" : "2022-10-06T22:18:52Z", "transaction_id" : "0" } ], "application_version" : "1", "creation_date" : "2022-10-06T22:18:53Z", "expiration_date" : "4001-01-01T00:00:00Z" } 2022-10-06 15:18:53.016783-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts 2022-10-06 15:18:53.017602-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts 2022-10-06 15:18:53.411689-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200 2022-10-06 15:18:53.413980-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished 2022-10-06 15:18:53.414147-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue 2022-10-06 15:18:53.414927-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins' 2022-10-06 15:18:53.415505-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ Vending CustomerInfo from cache. ```
With the fix, you can see transactions are now finished at the right time:
Open ``` 2022-10-07 11:08:01.967843-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins' 2022-10-07 11:08:02.123611-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple. 2022-10-07 11:08:02.138918-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/F02A9A20-949B-4C9D-A01E-AB0CC7544F96/data/Containers/Data/Application/C52A5EFD-733F-46EC-84B7-734B6AF9D28C/StoreKit/receipt 2022-10-07 11:08:02.139013-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-07 11:08:02.139095-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-07 11:08:02.139481-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:bd74b9d10f2c48eea1d741a32f5acb16 2022-10-07 11:08:02.147220-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started 2022-10-07 11:08:02.148192-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - INFO: ℹ️ Receipt parsed successfully 2022-10-07 11:08:02.148552-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: { "opaque_value" : "\/3nevQ0AAAA=", "sha1_hash" : "\/bAkxLQ6BPgUg3rR9jDR28dEfig=", "bundle_id" : "com.revenuecat.StoreKitTestApp", "in_app_purchases" : [ { "quantity" : 1, "product_id" : "consumable.10_coins", "purchase_date" : "2022-10-07T18:08:02Z", "transaction_id" : "0" } ], "application_version" : "1", "creation_date" : "2022-10-07T18:08:02Z", "expiration_date" : "4001-01-01T00:00:00Z" } 2022-10-07 11:08:02.148604-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts 2022-10-07 11:08:02.149748-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts 2022-10-07 11:08:02.733225-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200 2022-10-07 11:08:02.734687-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished 2022-10-07 11:08:02.734802-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue 2022-10-07 11:08:02.735631-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Sending updated CustomerInfo to delegate. 2022-10-07 11:08:02.735700-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - INFO: 💰 Finishing transaction '0' for product 'consumable.10_coins' 2022-10-07 11:08:02.760613-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins' 2022-10-07 11:08:02.760828-0700 BackendIntegrationTestsHostApp[45714:674590] [Purchases] - DEBUG: ℹ️ Vending Offerings from cache 2022-10-07 11:08:02.761032-0700 BackendIntegrationTestsHostApp[45714:674590] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins' 2022-10-07 11:08:02.820835-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple. 2022-10-07 11:08:02.839184-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/F02A9A20-949B-4C9D-A01E-AB0CC7544F96/data/Containers/Data/Application/C52A5EFD-733F-46EC-84B7-734B6AF9D28C/StoreKit/receipt 2022-10-07 11:08:02.839275-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-07 11:08:02.839348-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"] 2022-10-07 11:08:02.839476-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:bd74b9d10f2c48eea1d741a32f5acb16 2022-10-07 11:08:02.839911-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started 2022-10-07 11:08:02.840457-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - INFO: ℹ️ Receipt parsed successfully 2022-10-07 11:08:02.840603-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: { "opaque_value" : "fXr9\/QYAAAA=", "sha1_hash" : "zreYwGKGSaOEww1bsTRu8MylAas=", "bundle_id" : "com.revenuecat.StoreKitTestApp", "in_app_purchases" : [ { "quantity" : 1, "product_id" : "consumable.10_coins", "purchase_date" : "2022-10-07T18:08:02Z", "transaction_id" : "1" } ], "application_version" : "1", "creation_date" : "2022-10-07T18:08:02Z", "expiration_date" : "4001-01-01T00:00:00Z" } 2022-10-07 11:08:02.840662-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts 2022-10-07 11:08:02.841268-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts 2022-10-07 11:08:03.313116-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200 2022-10-07 11:08:03.316115-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished 2022-10-07 11:08:03.316342-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue 2022-10-07 11:08:03.317444-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Sending updated CustomerInfo to delegate. 2022-10-07 11:08:03.317527-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - INFO: 💰 Finishing transaction '1' for product 'consumable.10_coins' 2022-10-07 11:08:03.342767-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins' ```
[TRIAGE-134]: https://revenuecats.atlassian.net/browse/TRIAGE-134?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TRIAGE-131]: https://revenuecats.atlassian.net/browse/TRIAGE-131?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TRIAGE-82]: https://revenuecats.atlassian.net/browse/TRIAGE-82?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- 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 | 59 +++++++++++++++++++ ...rationPurchaseTesterConfiguration.storekit | 15 +++++ .../Mocks/MockStoreKit1Wrapper.swift | 4 +- .../Purchases/PurchasesPurchasingTests.swift | 39 +++++++++--- 10 files changed, 148 insertions(+), 31 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..8447d2108f 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 @Sendable () -> Void) { + completion() } } diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index 023ac556c0..912d74ed8b 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -734,7 +734,7 @@ private extension PurchasesOrchestrator { } } - self.finishTransactionIfNeeded(storeTransaction) + self.finishTransactionIfNeeded(storeTransaction, completion: {}) } func handleDeferredTransaction(_ transaction: SKPaymentTransaction) { @@ -899,23 +899,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() } } } @@ -1044,10 +1051,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..952bc99065 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 @Sendable () -> Void) { wrapper.finishTransaction(self.underlyingSK1Transaction) + completion() } } diff --git a/Sources/Purchasing/StoreKitAbstractions/SK2StoreTransaction.swift b/Sources/Purchasing/StoreKitAbstractions/SK2StoreTransaction.swift index f97a2d2ba4..b3d847884f 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 @Sendable () -> 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 59198085f0..0d99922776 100644 --- a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift @@ -68,6 +68,47 @@ class StoreKit1IntegrationTests: BaseBackendIntegrationTests { try await self.purchaseMonthlyProduct() } + func testCanPurchaseConsumable() async throws { + let info = try await self.purchaseConsumablePackage().customerInfo + + expect(info.allPurchasedProductIdentifiers).to(contain(Self.consumable10Coins)) + } + + 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"), + 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) + } + @discardableResult func verifyEntitlementWentThrough( _ customerInfo: CustomerInfo, diff --git a/Tests/TestingApps/PurchaseTester/RevenueCat_IntegrationPurchaseTesterConfiguration.storekit b/Tests/TestingApps/PurchaseTester/RevenueCat_IntegrationPurchaseTesterConfiguration.storekit index da62d85564..871b9872b0 100644 --- a/Tests/TestingApps/PurchaseTester/RevenueCat_IntegrationPurchaseTesterConfiguration.storekit +++ b/Tests/TestingApps/PurchaseTester/RevenueCat_IntegrationPurchaseTesterConfiguration.storekit @@ -18,6 +18,21 @@ "productID" : "lifetime", "referenceName" : "lifetime", "type" : "NonConsumable" + }, + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "67E2FE0B", + "localizations" : [ + { + "description" : "10 Coins", + "displayName" : "10 Coins", + "locale" : "en_US" + } + ], + "productID" : "consumable.10_coins", + "referenceName" : "10 coins", + "type" : "Consumable" } ], "settings" : { 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))