From 2130e99fd411d23202784d606f26a14625df4b02 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Wed, 15 Jan 2025 17:44:14 -0300 Subject: [PATCH 1/4] added a to-do in all references to main actor that don't have OS version checks --- Sources/Identity/CustomerInfoManager.swift | 1 + Sources/Misc/Concurrency/OperationDispatcher.swift | 2 ++ Sources/Misc/SystemInfo.swift | 3 +++ .../OfflineEntitlements/OfflineEntitlementsManager.swift | 2 ++ Sources/Purchasing/OfferingsManager.swift | 6 ++++++ Sources/Purchasing/Purchases/Purchases.swift | 1 + Sources/Purchasing/Purchases/TransactionPoster.swift | 2 ++ Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift | 4 +++- 8 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/Identity/CustomerInfoManager.swift b/Sources/Identity/CustomerInfoManager.swift index bea9ed8c64..cd88a6ff54 100644 --- a/Sources/Identity/CustomerInfoManager.swift +++ b/Sources/Identity/CustomerInfoManager.swift @@ -17,6 +17,7 @@ import Foundation class CustomerInfoManager { + // todo: remove main actor here typealias CustomerInfoCompletion = @MainActor @Sendable (Result) -> Void private let offlineEntitlementsManager: OfflineEntitlementsManager diff --git a/Sources/Misc/Concurrency/OperationDispatcher.swift b/Sources/Misc/Concurrency/OperationDispatcher.swift index 8f5f5d9f7b..855843cd01 100644 --- a/Sources/Misc/Concurrency/OperationDispatcher.swift +++ b/Sources/Misc/Concurrency/OperationDispatcher.swift @@ -52,6 +52,7 @@ class OperationDispatcher { self.mainQueue.async(execute: block) } + // todo: main actor references need OS check func dispatchOnMainActor(_ block: @MainActor @escaping @Sendable () -> Void) { Self.dispatchOnMainActor(block) } @@ -79,6 +80,7 @@ class OperationDispatcher { extension OperationDispatcher { + // todo: main actor needs OS check static func dispatchOnMainActor(_ block: @MainActor @escaping @Sendable () -> Void) { if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { Task { @MainActor in diff --git a/Sources/Misc/SystemInfo.swift b/Sources/Misc/SystemInfo.swift index 16b54ba38f..eab3bb328f 100644 --- a/Sources/Misc/SystemInfo.swift +++ b/Sources/Misc/SystemInfo.swift @@ -156,6 +156,7 @@ class SystemInfo { /// Synchronous API for callers in `@MainActor`. /// - Seealso: `isApplicationBackgrounded(completion:)` + // todo: main actor here @MainActor var isApplicationBackgrounded: Bool { #if os(iOS) || os(tvOS) || VISION_OS @@ -274,6 +275,7 @@ private extension SystemInfo { // iOS/tvOS App extensions can't access UIApplication.sharedApplication, and will fail to compile if any calls to // it are made. There are no pre-processor macros available to check if the code is running in an app extension, // so we check if we're running in an app extension at runtime, and if not, we use KVC to call sharedApplication. + // todo: main actor here @MainActor var isApplicationBackgroundedIOSAndTVOS: Bool { if self.isAppExtension { @@ -287,6 +289,7 @@ private extension SystemInfo { #elseif os(watchOS) @MainActor + // todo: main actor here var isApplicationBackgroundedWatchOS: Bool { var isSingleTargetApplication: Bool { return Bundle.main.infoDictionary?.keys.contains("WKApplication") == true diff --git a/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift b/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift index 22c1802ec7..44a8e5ccca 100644 --- a/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift +++ b/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift @@ -30,6 +30,7 @@ class OfflineEntitlementsManager { self.systemInfo = systemInfo } + // todo: main actor here func updateProductsEntitlementsCacheIfStale( isAppBackgrounded: Bool, completion: (@MainActor @Sendable (Result<(), Error>) -> Void)? @@ -103,6 +104,7 @@ private extension OfflineEntitlementsManager { private extension OfflineEntitlementsManager { + // todo: main actor here func dispatchCompletionOnMainThreadIfPossible( _ completion: (@MainActor @Sendable (Result) -> Void)?, result: Result diff --git a/Sources/Purchasing/OfferingsManager.swift b/Sources/Purchasing/OfferingsManager.swift index c2f57f7f95..5a7aa8d69b 100644 --- a/Sources/Purchasing/OfferingsManager.swift +++ b/Sources/Purchasing/OfferingsManager.swift @@ -39,6 +39,7 @@ class OfferingsManager { self.productsManager = productsManager } + // todo: main actor here func offerings( appUserID: String, fetchPolicy: FetchPolicy = .default, @@ -72,6 +73,7 @@ class OfferingsManager { return self.deviceCache.cachedOfferings } + // todo: main actor here func updateOfferingsCache( appUserID: String, isAppBackgrounded: Bool, @@ -132,6 +134,7 @@ class OfferingsManager { private extension OfferingsManager { + // todo: main actor here func fetchFromNetwork( appUserID: String, fetchPolicy: FetchPolicy = .default, @@ -225,6 +228,7 @@ private extension OfferingsManager { } } + // todo: main actor here func handleOfferingsBackendResult( with response: OfferingsResponse, appUserID: String, @@ -255,6 +259,7 @@ private extension OfferingsManager { } } + // todo: main actor here func handleOfferingsUpdateError( _ error: Error, completion: (@MainActor @Sendable (Result) -> Void)? @@ -264,6 +269,7 @@ private extension OfferingsManager { self.dispatchCompletionOnMainThreadIfPossible(completion, value: .failure(error)) } + // todo: main actor here func dispatchCompletionOnMainThreadIfPossible( _ completion: (@MainActor @Sendable (T) -> Void)?, value: T diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index a04fd3e022..c11bec8922 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -33,6 +33,7 @@ public typealias PurchaseResultData = (transaction: StoreTransaction?, /** Completion block for ``Purchases/purchase(product:completion:)`` */ +// todo: main actor here public typealias PurchaseCompletedBlock = @MainActor @Sendable (StoreTransaction?, CustomerInfo?, PublicError?, diff --git a/Sources/Purchasing/Purchases/TransactionPoster.swift b/Sources/Purchasing/Purchases/TransactionPoster.swift index 0ace188224..9da502dce0 100644 --- a/Sources/Purchasing/Purchases/TransactionPoster.swift +++ b/Sources/Purchasing/Purchases/TransactionPoster.swift @@ -47,6 +47,7 @@ protocol TransactionPosterType: AnyObject, Sendable { /// Finishes the transaction if not in observer mode. /// - Note: `handlePurchasedTransaction` calls this automatically, /// this is only required for failed transactions. + // todo: main actor here func finishTransactionIfNeeded( _ transaction: StoreTransactionType, completion: @escaping @Sendable @MainActor () -> Void @@ -121,6 +122,7 @@ final class TransactionPoster: TransactionPosterType { } } + // todo: main actor here func finishTransactionIfNeeded( _ transaction: StoreTransactionType, completion: @escaping @Sendable @MainActor () -> Void diff --git a/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift b/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift index 8f98ca9332..63f36766de 100644 --- a/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift +++ b/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift @@ -27,7 +27,8 @@ class StoreKitRequestFetcher: NSObject { private let requestFactory: ReceiptRefreshRequestFactory private var receiptRefreshRequest: SKRequest? - private var receiptRefreshCompletionHandlers: [@MainActor @Sendable () -> Void] + // todo: main actor here + private var receiptRefreshCompletionHandlers: [@Sendable () -> Void] private let operationDispatcher: OperationDispatcher init(requestFactory: ReceiptRefreshRequestFactory = ReceiptRefreshRequestFactory(), @@ -38,6 +39,7 @@ class StoreKitRequestFetcher: NSObject { self.receiptRefreshCompletionHandlers = [] } + // todo: main actor here func fetchReceiptData(_ completion: @MainActor @Sendable @escaping () -> Void) { self.operationDispatcher.dispatchOnWorkerThread { self.receiptRefreshCompletionHandlers.append(completion) From d678bf4ac69760695b14437681edd27e248ad792 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Fri, 17 Jan 2025 16:16:46 -0300 Subject: [PATCH 2/4] replaced main actor with main thread in a handful of places --- Sources/Identity/CustomerInfoManager.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Identity/CustomerInfoManager.swift b/Sources/Identity/CustomerInfoManager.swift index cd88a6ff54..e9396039e4 100644 --- a/Sources/Identity/CustomerInfoManager.swift +++ b/Sources/Identity/CustomerInfoManager.swift @@ -18,7 +18,7 @@ import Foundation class CustomerInfoManager { // todo: remove main actor here - typealias CustomerInfoCompletion = @MainActor @Sendable (Result) -> Void + typealias CustomerInfoCompletion = @Sendable (Result) -> Void private let offlineEntitlementsManager: OfflineEntitlementsManager private let operationDispatcher: OperationDispatcher @@ -90,7 +90,7 @@ class CustomerInfoManager { } if let completion = completion { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchAsyncOnMainThread { completion(result) } } @@ -115,7 +115,7 @@ class CustomerInfoManager { } if let completion = completion { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchAsyncOnMainThread { completion(.success(customerInfo)) } } @@ -129,7 +129,7 @@ class CustomerInfoManager { ) { switch fetchPolicy { case .fromCacheOnly: - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchAsyncOnMainThread { completion?( Result(self.cachedCustomerInfo(appUserID: appUserID), .missingCachedCustomerInfo()) ) @@ -150,7 +150,7 @@ class CustomerInfoManager { Logger.debug(Strings.customerInfo.vending_cache) if let completion = completion { completionCalled = true - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchAsyncOnMainThread { completion(.success(infoFromCache)) } } @@ -176,7 +176,7 @@ class CustomerInfoManager { if let infoFromCache = infoFromCache, !isCacheStale { Logger.debug(Strings.customerInfo.vending_cache) if let completion = completion { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchAsyncOnMainThread { completion(.success(infoFromCache)) } } From f4474314cda8063a0a17666ddce6f6de8970b0e2 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Fri, 17 Jan 2025 18:30:07 -0300 Subject: [PATCH 3/4] replace all references to mainActor with dispatching to main thread --- Sources/Identity/CustomerInfoManager.swift | 2 +- .../Misc/Concurrency/OperationDispatcher.swift | 2 +- Sources/Misc/SystemInfo.swift | 7 ++----- .../OfflineEntitlementsManager.swift | 6 +++--- Sources/Purchasing/OfferingsManager.swift | 16 ++++++++-------- Sources/Purchasing/Purchases/Purchases.swift | 8 ++++---- .../Purchases/PurchasesOrchestrator.swift | 6 ++++-- .../Purchasing/Purchases/TransactionPoster.swift | 7 ++++--- .../StoreKit1/StoreKitRequestFetcher.swift | 4 +++- 9 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Sources/Identity/CustomerInfoManager.swift b/Sources/Identity/CustomerInfoManager.swift index e9396039e4..c0d3f5881c 100644 --- a/Sources/Identity/CustomerInfoManager.swift +++ b/Sources/Identity/CustomerInfoManager.swift @@ -17,7 +17,7 @@ import Foundation class CustomerInfoManager { - // todo: remove main actor here + // todo: confirm removal of mainActor here typealias CustomerInfoCompletion = @Sendable (Result) -> Void private let offlineEntitlementsManager: OfflineEntitlementsManager diff --git a/Sources/Misc/Concurrency/OperationDispatcher.swift b/Sources/Misc/Concurrency/OperationDispatcher.swift index 855843cd01..3bae8948e6 100644 --- a/Sources/Misc/Concurrency/OperationDispatcher.swift +++ b/Sources/Misc/Concurrency/OperationDispatcher.swift @@ -52,7 +52,7 @@ class OperationDispatcher { self.mainQueue.async(execute: block) } - // todo: main actor references need OS check + // todo: confirm all references to this have OS checks func dispatchOnMainActor(_ block: @MainActor @escaping @Sendable () -> Void) { Self.dispatchOnMainActor(block) } diff --git a/Sources/Misc/SystemInfo.swift b/Sources/Misc/SystemInfo.swift index eab3bb328f..9fb23fce29 100644 --- a/Sources/Misc/SystemInfo.swift +++ b/Sources/Misc/SystemInfo.swift @@ -149,7 +149,7 @@ class SystemInfo { /// Asynchronous API if caller can't ensure that it's invoked in the `@MainActor` /// - Seealso: `isApplicationBackgrounded` func isApplicationBackgrounded(completion: @escaping @Sendable (Bool) -> Void) { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchOnMainThread { completion(self.isApplicationBackgrounded) } } @@ -157,8 +157,7 @@ class SystemInfo { /// Synchronous API for callers in `@MainActor`. /// - Seealso: `isApplicationBackgrounded(completion:)` // todo: main actor here - @MainActor - var isApplicationBackgrounded: Bool { + private var isApplicationBackgrounded: Bool { #if os(iOS) || os(tvOS) || VISION_OS return self.isApplicationBackgroundedIOSAndTVOS #elseif os(macOS) @@ -276,7 +275,6 @@ private extension SystemInfo { // it are made. There are no pre-processor macros available to check if the code is running in an app extension, // so we check if we're running in an app extension at runtime, and if not, we use KVC to call sharedApplication. // todo: main actor here - @MainActor var isApplicationBackgroundedIOSAndTVOS: Bool { if self.isAppExtension { return true @@ -288,7 +286,6 @@ private extension SystemInfo { #elseif os(watchOS) - @MainActor // todo: main actor here var isApplicationBackgroundedWatchOS: Bool { var isSingleTargetApplication: Bool { diff --git a/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift b/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift index 44a8e5ccca..ca7de5f3e9 100644 --- a/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift +++ b/Sources/OfflineEntitlements/OfflineEntitlementsManager.swift @@ -33,7 +33,7 @@ class OfflineEntitlementsManager { // todo: main actor here func updateProductsEntitlementsCacheIfStale( isAppBackgrounded: Bool, - completion: (@MainActor @Sendable (Result<(), Error>) -> Void)? + completion: (@Sendable (Result<(), Error>) -> Void)? ) { guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *), !self.systemInfo.observerMode, @@ -106,11 +106,11 @@ private extension OfflineEntitlementsManager { // todo: main actor here func dispatchCompletionOnMainThreadIfPossible( - _ completion: (@MainActor @Sendable (Result) -> Void)?, + _ completion: (@Sendable (Result) -> Void)?, result: Result ) { if let completion = completion { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchOnMainThread { completion(result) } } diff --git a/Sources/Purchasing/OfferingsManager.swift b/Sources/Purchasing/OfferingsManager.swift index 5a7aa8d69b..5d68550aea 100644 --- a/Sources/Purchasing/OfferingsManager.swift +++ b/Sources/Purchasing/OfferingsManager.swift @@ -44,7 +44,7 @@ class OfferingsManager { appUserID: String, fetchPolicy: FetchPolicy = .default, fetchCurrent: Bool = false, - completion: (@MainActor @Sendable (Result) -> Void)? + completion: (@Sendable (Result) -> Void)? ) { guard !fetchCurrent else { self.fetchFromNetwork(appUserID: appUserID, fetchPolicy: fetchPolicy, completion: completion) @@ -78,7 +78,7 @@ class OfferingsManager { appUserID: String, isAppBackgrounded: Bool, fetchPolicy: FetchPolicy = .default, - completion: (@MainActor @Sendable (Result) -> Void)? + completion: (@Sendable (Result) -> Void)? ) { self.backend.offerings.getOfferings(appUserID: appUserID, isAppBackgrounded: isAppBackgrounded) { result in switch result { @@ -138,7 +138,7 @@ private extension OfferingsManager { func fetchFromNetwork( appUserID: String, fetchPolicy: FetchPolicy = .default, - completion: (@MainActor @Sendable (Result) -> Void)? + completion: (@Sendable (Result) -> Void)? ) { Logger.debug(Strings.offering.no_cached_offerings_fetching_from_network) @@ -233,7 +233,7 @@ private extension OfferingsManager { with response: OfferingsResponse, appUserID: String, fetchPolicy: FetchPolicy, - completion: (@MainActor @Sendable (Result) -> Void)? + completion: (@Sendable (Result) -> Void)? ) { self.createOfferings(from: response, fetchPolicy: fetchPolicy) { result in switch result { @@ -262,20 +262,20 @@ private extension OfferingsManager { // todo: main actor here func handleOfferingsUpdateError( _ error: Error, - completion: (@MainActor @Sendable (Result) -> Void)? + completion: (@Sendable (Result) -> Void)? ) { Logger.appleError(Strings.offering.fetching_offerings_error(error: error, underlyingError: error.underlyingError)) self.dispatchCompletionOnMainThreadIfPossible(completion, value: .failure(error)) } - // todo: main actor here + // todo: verify main actor replacement here func dispatchCompletionOnMainThreadIfPossible( - _ completion: (@MainActor @Sendable (T) -> Void)?, + _ completion: (@Sendable (T) -> Void)?, value: T ) { if let completion = completion { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchOnMainThread { completion(value) } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index c11bec8922..d2afe3d6e8 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -34,10 +34,10 @@ public typealias PurchaseResultData = (transaction: StoreTransaction?, Completion block for ``Purchases/purchase(product:completion:)`` */ // todo: main actor here -public typealias PurchaseCompletedBlock = @MainActor @Sendable (StoreTransaction?, - CustomerInfo?, - PublicError?, - Bool) -> Void +public typealias PurchaseCompletedBlock = @Sendable (StoreTransaction?, + CustomerInfo?, + PublicError?, + Bool) -> Void /** Block for starting purchases in ``PurchasesDelegate/purchases(_:readyForPromotedProduct:purchase:)`` diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index d7f7e4e433..a608f9c3b9 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -374,8 +374,9 @@ final class PurchasesOrchestrator { * we wouldn't be able to notify of the purchase result. */ + // todo: verify actor replacement here guard let productIdentifier = payment.extractProductIdentifier() else { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchOnMainThread { completion( nil, nil, @@ -1291,8 +1292,9 @@ private extension PurchasesOrchestrator { } } + // todo: verify actor replacement here func handleTestProduct(_ completion: @escaping PurchaseCompletedBlock) { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchOnMainThread { completion( nil, nil, diff --git a/Sources/Purchasing/Purchases/TransactionPoster.swift b/Sources/Purchasing/Purchases/TransactionPoster.swift index 9da502dce0..0330a3616d 100644 --- a/Sources/Purchasing/Purchases/TransactionPoster.swift +++ b/Sources/Purchasing/Purchases/TransactionPoster.swift @@ -50,7 +50,7 @@ protocol TransactionPosterType: AnyObject, Sendable { // todo: main actor here func finishTransactionIfNeeded( _ transaction: StoreTransactionType, - completion: @escaping @Sendable @MainActor () -> Void + completion: @escaping @Sendable () -> Void ) } @@ -125,11 +125,11 @@ final class TransactionPoster: TransactionPosterType { // todo: main actor here func finishTransactionIfNeeded( _ transaction: StoreTransactionType, - completion: @escaping @Sendable @MainActor () -> Void + completion: @escaping @Sendable () -> Void ) { @Sendable func complete() { - self.operationDispatcher.dispatchOnMainActor(completion) + self.operationDispatcher.dispatchOnMainThread(completion) } guard self.finishTransactions else { @@ -332,6 +332,7 @@ extension TransactionPoster { private extension TransactionPoster { + // todo: verify actor replacement ehre func product(with identifier: String, completion: @escaping (StoreProduct?) -> Void) { self.productsManager.products(withIdentifiers: [identifier]) { products in self.operationDispatcher.dispatchOnMainThread { diff --git a/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift b/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift index 63f36766de..c856824e24 100644 --- a/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift +++ b/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift @@ -29,6 +29,7 @@ class StoreKitRequestFetcher: NSObject { private var receiptRefreshRequest: SKRequest? // todo: main actor here private var receiptRefreshCompletionHandlers: [@Sendable () -> Void] + private let operationDispatcher: OperationDispatcher init(requestFactory: ReceiptRefreshRequestFactory = ReceiptRefreshRequestFactory(), @@ -95,8 +96,9 @@ private extension StoreKitRequestFetcher { let completionHandlers = self.receiptRefreshCompletionHandlers self.receiptRefreshCompletionHandlers = [] + // todo: verify main actor replacement here for handler in completionHandlers { - self.operationDispatcher.dispatchOnMainActor { + self.operationDispatcher.dispatchOnMainThread { handler() } } From f2e18e6389f7ff6a22e03ec4cb1af3822587e931 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Fri, 17 Jan 2025 18:46:02 -0300 Subject: [PATCH 4/4] missed one --- Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift b/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift index c856824e24..11dff4eecc 100644 --- a/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift +++ b/Sources/Purchasing/StoreKit1/StoreKitRequestFetcher.swift @@ -41,7 +41,7 @@ class StoreKitRequestFetcher: NSObject { } // todo: main actor here - func fetchReceiptData(_ completion: @MainActor @Sendable @escaping () -> Void) { + func fetchReceiptData(_ completion: @Sendable @escaping () -> Void) { self.operationDispatcher.dispatchOnWorkerThread { self.receiptRefreshCompletionHandlers.append(completion)