From 43de2b67969bdd31cd59e46e2b4bf753c930536c Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 6 Aug 2024 09:51:00 -0500 Subject: [PATCH 01/15] Move from DispatchQueue to actor for thread-safety --- .../ConfigurationCallbackStorage.swift | 13 ++--- .../Configuration/ConfigurationLoader.swift | 50 ++++++++++--------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift b/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift index 5f4d540d7c..c936b26ef7 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift @@ -1,26 +1,23 @@ import Foundation /// Used to store, access, and manage an array of to-be-invoked `BTConfiguration` GET result callbacks in a thread-safe manner -class ConfigurationCallbackStorage { +actor ConfigurationCallbackStorage { - private let queue = DispatchQueue(label: "com.braintreepayments.ConfigurationCallbackStorage") private var pendingCompletions: [(BTConfiguration?, Error?) -> Void] = [] /// The number of completions that are waiting to be invoked var count: Int { - queue.sync { pendingCompletions.count } + pendingCompletions.count } /// Adds a pending, yet to-be-invoked completion handler func add(_ completion: @escaping (BTConfiguration?, Error?) -> Void) { - queue.sync { pendingCompletions.append(completion) } + pendingCompletions.append(completion) } /// Executes and clears all pending completion handlers func invoke(_ configuration: BTConfiguration?, _ error: Error?) { - queue.sync { - pendingCompletions.forEach { $0(configuration, error) } - pendingCompletions.removeAll() - } + pendingCompletions.forEach { $0(configuration, error) } + pendingCompletions.removeAll() } } diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index ecc15220f6..5c16a124cb 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -41,30 +41,32 @@ class ConfigurationLoader { return } - pendingCompletions.add(completion) - - // If this is the 1st `v1/config` GET attempt, proceed with firing the network request. - // Otherwise, there is already a pending network request. - if pendingCompletions.count == 1 { - http.get(configPath, parameters: BTConfigurationRequest()) { [weak self] body, response, error in - guard let self else { - self?.notifyCompletions(nil, BTAPIClientError.deallocated) - return - } - - if let error { - notifyCompletions(nil, error) - return - } else if response?.statusCode != 200 || body == nil { - notifyCompletions(nil, BTAPIClientError.configurationUnavailable) - return - } else { - let configuration = BTConfiguration(json: body) - - try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration) + Task { + await pendingCompletions.add(completion) + + // If this is the 1st `v1/config` GET attempt, proceed with firing the network request. + // Otherwise, there is already a pending network request. + if await pendingCompletions.count == 1 { + http.get(configPath, parameters: BTConfigurationRequest()) { [weak self] body, response, error in + guard let self else { + self?.notifyCompletions(nil, BTAPIClientError.deallocated) + return + } - notifyCompletions(configuration, nil) - return + if let error { + notifyCompletions(nil, error) + return + } else if response?.statusCode != 200 || body == nil { + notifyCompletions(nil, BTAPIClientError.configurationUnavailable) + return + } else { + let configuration = BTConfiguration(json: body) + + try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration) + + notifyCompletions(configuration, nil) + return + } } } } @@ -85,6 +87,6 @@ class ConfigurationLoader { // MARK: - Private Methods func notifyCompletions(_ configuration: BTConfiguration?, _ error: Error?) { - pendingCompletions.invoke(configuration, error) + Task { await pendingCompletions.invoke(configuration, error) } } } From 8f512343daf2000c74f348872c8faf3eee70d693 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 6 Aug 2024 09:55:41 -0500 Subject: [PATCH 02/15] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b8e30601..2d8fb02c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## unreleased * BraintreeCore * Fix bug where some analytics wouldn't send if `BTAPIClient` instantiated on button click + * Fix low-memory crash in ConfigurationCallbackStorage (fixes #1382) ## 6.23.2 (2024-07-30) * BraintreePayPal From d546f2e2cf4e85dbd0fcd31c3818295de2c0d020 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 6 Aug 2024 10:10:28 -0500 Subject: [PATCH 03/15] Revert actor changes; don't use any thread-safety wrapper on config callback array --- .../ConfigurationCallbackStorage.swift | 4 +- .../Configuration/ConfigurationLoader.swift | 50 +++++++++---------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift b/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift index c936b26ef7..a2ccbb430a 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift @@ -1,7 +1,7 @@ import Foundation -/// Used to store, access, and manage an array of to-be-invoked `BTConfiguration` GET result callbacks in a thread-safe manner -actor ConfigurationCallbackStorage { +/// Used to store, access, and manage an array of to-be-invoked `BTConfiguration` GET result callbacks +class ConfigurationCallbackStorage { private var pendingCompletions: [(BTConfiguration?, Error?) -> Void] = [] diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 5c16a124cb..ecc15220f6 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -41,32 +41,30 @@ class ConfigurationLoader { return } - Task { - await pendingCompletions.add(completion) - - // If this is the 1st `v1/config` GET attempt, proceed with firing the network request. - // Otherwise, there is already a pending network request. - if await pendingCompletions.count == 1 { - http.get(configPath, parameters: BTConfigurationRequest()) { [weak self] body, response, error in - guard let self else { - self?.notifyCompletions(nil, BTAPIClientError.deallocated) - return - } + pendingCompletions.add(completion) + + // If this is the 1st `v1/config` GET attempt, proceed with firing the network request. + // Otherwise, there is already a pending network request. + if pendingCompletions.count == 1 { + http.get(configPath, parameters: BTConfigurationRequest()) { [weak self] body, response, error in + guard let self else { + self?.notifyCompletions(nil, BTAPIClientError.deallocated) + return + } + + if let error { + notifyCompletions(nil, error) + return + } else if response?.statusCode != 200 || body == nil { + notifyCompletions(nil, BTAPIClientError.configurationUnavailable) + return + } else { + let configuration = BTConfiguration(json: body) + + try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration) - if let error { - notifyCompletions(nil, error) - return - } else if response?.statusCode != 200 || body == nil { - notifyCompletions(nil, BTAPIClientError.configurationUnavailable) - return - } else { - let configuration = BTConfiguration(json: body) - - try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration) - - notifyCompletions(configuration, nil) - return - } + notifyCompletions(configuration, nil) + return } } } @@ -87,6 +85,6 @@ class ConfigurationLoader { // MARK: - Private Methods func notifyCompletions(_ configuration: BTConfiguration?, _ error: Error?) { - Task { await pendingCompletions.invoke(configuration, error) } + pendingCompletions.invoke(configuration, error) } } From 8f8b5ab315c85182df5803b888b62d307c639658 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 7 Aug 2024 15:35:28 -0500 Subject: [PATCH 04/15] WIP - Use while loop to prevent duplicate config GET requests & bool flag Signed-off-by: Jax DesMarais-Leder --- Sources/BraintreeCore/BTAPIClient.swift | 18 ++++----- Sources/BraintreeCore/BTHTTP.swift | 13 +++++++ .../ConfigurationCallbackStorage.swift | 3 ++ .../Configuration/ConfigurationLoader.swift | 38 +++++++++++++++---- 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 4479a94a05..92ddfce900 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -93,19 +93,15 @@ import Foundation /// cached on subsequent calls for better performance. @_documentation(visibility: private) public func fetchOrReturnRemoteConfiguration(_ completion: @escaping (BTConfiguration?, Error?) -> Void) { - configurationLoader.getConfig { [weak self] configuration, error in - guard let self else { - completion(nil, BTAPIClientError.deallocated) - return - } - - if let error { + // TODO: - Consider updating all feature clients to use async version of this method? + Task { + do { + let configuration = try await configurationLoader.getConfig() + setupHTTPCredentials(configuration) + completion(configuration, nil) + } catch { completion(nil, error) - return } - - setupHTTPCredentials(configuration) - completion(configuration, nil) } } diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index ef72f275e9..b7df2773f1 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -77,6 +77,19 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { completion(nil, nil, error) } } + + func get(_ path: String, configuration: BTConfiguration? = nil, parameters: Encodable? = nil) async throws -> (BTJSON, HTTPURLResponse) { + try await withCheckedThrowingContinuation { continuation in + get(path, configuration: configuration, parameters: parameters) { body, response, error in + if let error { + continuation.resume(throwing: error) + } else if let body, let response { + continuation.resume(returning: (body, response)) + } + } + // TODO: - How do we want to handle case of nil body or response + } + } // TODO: - Remove when all POST bodies use Codable, instead of BTJSON/raw dictionaries func post( diff --git a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift b/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift index a2ccbb430a..1b6b5ce132 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift @@ -1,8 +1,11 @@ import Foundation +// TODO: - Do we still need this class? It no-longer wraps an array to provide thread-safety + /// Used to store, access, and manage an array of to-be-invoked `BTConfiguration` GET result callbacks class ConfigurationCallbackStorage { + // TODO: - Remove. Not an option to store array of completions. private var pendingCompletions: [(BTConfiguration?, Error?) -> Void] = [] /// The number of completions that are waiting to be invoked diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index ecc15220f6..5b667a3b3f 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -8,7 +8,8 @@ class ConfigurationLoader { private let configurationCache = ConfigurationCache.shared private let http: BTHTTP private let pendingCompletions = ConfigurationCallbackStorage() - + private var isConfigCached = false // TODO: - Rename; should this bool live here? + // MARK: - Intitializer init(http: BTHTTP) { @@ -69,17 +70,38 @@ class ConfigurationLoader { } } } - + func getConfig() async throws -> BTConfiguration { - try await withCheckedThrowingContinuation { continuation in - getConfig { configuration, error in - if let error { - continuation.resume(throwing: error) - } else if let configuration { - continuation.resume(returning: configuration) + if let cachedConfig = try? configurationCache.getFromCache(authorization: http.authorization.bearer) { + isConfigCached = true + return cachedConfig + } + + while !isConfigCached { + print("While loop body") + + do { + print("🤞GET request made") + let (body, response) = try await http.get(configPath, parameters: BTConfigurationRequest()) + + if response.statusCode != 200 { // || body == nil { + throw BTAPIClientError.configurationUnavailable + } else { + let configuration = BTConfiguration(json: body) + + try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration) + + NotificationCenter.default.post(name: Notification.Name("ConfigGet"), object: configuration) + isConfigCached = true + + return configuration } + } catch { + throw error } } + print("Exited while loop") + throw BTAPIClientError.configurationUnavailable } // MARK: - Private Methods From 3b68a60b5944e99d58776d7e92d40228f7d10b40 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 7 Aug 2024 16:21:49 -0500 Subject: [PATCH 05/15] remove ConfigurationCallbackStorage; cleanup --- Braintree.xcodeproj/project.pbxproj | 4 -- .../Features/ApplePayViewController.swift | 1 + Sources/BraintreeCore/BTHTTP.swift | 6 ++- .../ConfigurationCallbackStorage.swift | 26 ----------- .../Configuration/ConfigurationLoader.swift | 44 +------------------ 5 files changed, 7 insertions(+), 74 deletions(-) delete mode 100644 Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index da061a903b..b335d61bf7 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -117,7 +117,6 @@ 80581A8C25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */; }; 80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; }; 806C85632B90EBED00A2754C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 806C85622B90EBED00A2754C /* PrivacyInfo.xcprivacy */; }; - 807296042C41B63F0093F2AB /* ConfigurationCallbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807296032C41B63F0093F2AB /* ConfigurationCallbackStorage.swift */; }; 8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; }; 80842DA72B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */; }; 8087C10F2BFBACCA0020FC2E /* TokenizationKey_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */; }; @@ -857,7 +856,6 @@ 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsClient_Tests.swift; sourceTree = ""; }; 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsRequest.swift; sourceTree = ""; }; 806C85622B90EBED00A2754C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - 807296032C41B63F0093F2AB /* ConfigurationCallbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationCallbackStorage.swift; sourceTree = ""; }; 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAPIRequest.swift; sourceTree = ""; }; 80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenizationKey_Tests.swift; sourceTree = ""; }; @@ -1509,7 +1507,6 @@ BE5C8C0228A2C183004F9130 /* BTConfiguration+Core.swift */, 804DC45C2B2D08FF00F17A15 /* BTConfigurationRequest.swift */, BE698EA128AA8EEA001D9B10 /* ConfigurationCache.swift */, - 807296032C41B63F0093F2AB /* ConfigurationCallbackStorage.swift */, 45EFC3962C2DBF32005E7F5B /* ConfigurationLoader.swift */, ); path = Configuration; @@ -3280,7 +3277,6 @@ 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */, BED00CB028A579D700D74AEC /* BTClientToken.swift in Sources */, 5708E0A628809AD9007946B9 /* BTJSON.swift in Sources */, - 807296042C41B63F0093F2AB /* ConfigurationCallbackStorage.swift in Sources */, 574891ED286F7ECA0020DA36 /* BTClientMetadata.swift in Sources */, BE24C66E28E49A730067B11A /* BTAPIClientError.swift in Sources */, BC17F9BC28D24C9E004B18CC /* BTGraphQLErrorTree.swift in Sources */, diff --git a/Demo/Application/Features/ApplePayViewController.swift b/Demo/Application/Features/ApplePayViewController.swift index 3c1bab9f7b..48e0499411 100644 --- a/Demo/Application/Features/ApplePayViewController.swift +++ b/Demo/Application/Features/ApplePayViewController.swift @@ -40,6 +40,7 @@ class ApplePayViewController: PaymentButtonBaseViewController { paymentAuthorizationViewController.delegate = self self.progressBlock("Presenting Apple Pay Sheet") + // TODO: why does this need dispatch queue main self.present(paymentAuthorizationViewController, animated: true) } } diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index b7df2773f1..dfb2875320 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -78,7 +78,11 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { } } - func get(_ path: String, configuration: BTConfiguration? = nil, parameters: Encodable? = nil) async throws -> (BTJSON, HTTPURLResponse) { + func get( + _ path: String, + configuration: BTConfiguration? = nil, + parameters: Encodable? = nil + ) async throws -> (BTJSON, HTTPURLResponse) { try await withCheckedThrowingContinuation { continuation in get(path, configuration: configuration, parameters: parameters) { body, response, error in if let error { diff --git a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift b/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift deleted file mode 100644 index 1b6b5ce132..0000000000 --- a/Sources/BraintreeCore/Configuration/ConfigurationCallbackStorage.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -// TODO: - Do we still need this class? It no-longer wraps an array to provide thread-safety - -/// Used to store, access, and manage an array of to-be-invoked `BTConfiguration` GET result callbacks -class ConfigurationCallbackStorage { - - // TODO: - Remove. Not an option to store array of completions. - private var pendingCompletions: [(BTConfiguration?, Error?) -> Void] = [] - - /// The number of completions that are waiting to be invoked - var count: Int { - pendingCompletions.count - } - - /// Adds a pending, yet to-be-invoked completion handler - func add(_ completion: @escaping (BTConfiguration?, Error?) -> Void) { - pendingCompletions.append(completion) - } - - /// Executes and clears all pending completion handlers - func invoke(_ configuration: BTConfiguration?, _ error: Error?) { - pendingCompletions.forEach { $0(configuration, error) } - pendingCompletions.removeAll() - } -} diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 5b667a3b3f..9321de0cc0 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -7,10 +7,9 @@ class ConfigurationLoader { private let configPath = "v1/configuration" private let configurationCache = ConfigurationCache.shared private let http: BTHTTP - private let pendingCompletions = ConfigurationCallbackStorage() private var isConfigCached = false // TODO: - Rename; should this bool live here? - // MARK: - Intitializer + // MARK: - Initializer init(http: BTHTTP) { self.http = http @@ -36,41 +35,6 @@ class ConfigurationLoader { /// - `BTConfiguration?`: The configuration object if it is successfully fetched or retrieved from the cache. /// - `Error?`: An error object if fetching the configuration fails or if the instance is deallocated. @_documentation(visibility: private) - func getConfig(completion: @escaping (BTConfiguration?, Error?) -> Void) { - if let cachedConfig = try? configurationCache.getFromCache(authorization: http.authorization.bearer) { - completion(cachedConfig, nil) - return - } - - pendingCompletions.add(completion) - - // If this is the 1st `v1/config` GET attempt, proceed with firing the network request. - // Otherwise, there is already a pending network request. - if pendingCompletions.count == 1 { - http.get(configPath, parameters: BTConfigurationRequest()) { [weak self] body, response, error in - guard let self else { - self?.notifyCompletions(nil, BTAPIClientError.deallocated) - return - } - - if let error { - notifyCompletions(nil, error) - return - } else if response?.statusCode != 200 || body == nil { - notifyCompletions(nil, BTAPIClientError.configurationUnavailable) - return - } else { - let configuration = BTConfiguration(json: body) - - try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration) - - notifyCompletions(configuration, nil) - return - } - } - } - } - func getConfig() async throws -> BTConfiguration { if let cachedConfig = try? configurationCache.getFromCache(authorization: http.authorization.bearer) { isConfigCached = true @@ -103,10 +67,4 @@ class ConfigurationLoader { print("Exited while loop") throw BTAPIClientError.configurationUnavailable } - - // MARK: - Private Methods - - func notifyCompletions(_ configuration: BTConfiguration?, _ error: Error?) { - pendingCompletions.invoke(configuration, error) - } } From 97d769f71095acebe2484cb249f0558eacf3eac3 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 7 Aug 2024 17:06:59 -0500 Subject: [PATCH 06/15] WIP - start updating tests --- Sources/BraintreeCore/BTHTTP.swift | 4 +- .../ConfigurationLoader_Tests.swift | 88 +++++++++---------- .../MockConfigurationLoader.swift | 12 +-- 3 files changed, 45 insertions(+), 59 deletions(-) diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index dfb2875320..e32e03756e 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -87,8 +87,8 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { get(path, configuration: configuration, parameters: parameters) { body, response, error in if let error { continuation.resume(throwing: error) - } else if let body, let response { - continuation.resume(returning: (body, response)) + } else if let response { + continuation.resume(returning: (body ?? BTJSON(), response)) } } // TODO: - How do we want to handle case of nil body or response diff --git a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift index a0198fc5bf..b3accd0ad6 100644 --- a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift @@ -20,89 +20,81 @@ class ConfigurationLoader_Tests: XCTestCase { super.tearDown() } - func testGetConfig_whenCached_returnsConfigFromCache() { + func testGetConfig_whenCached_returnsConfigFromCache() async { let sampleJSON = ["test": "value", "environment": "fake-env1"] try? ConfigurationCache.shared.putInCache(authorization: "development_tokenization_key", configuration: BTConfiguration(json: BTJSON(value: sampleJSON))) - let expectation = expectation(description: "Callback invoked") - sut.getConfig { configuration, error in - XCTAssertEqual(configuration?.environment, "fake-env1") - XCTAssertEqual(configuration?.json?["test"].asString(), "value") + do { + let configuration = try await sut.getConfig() + XCTAssertEqual(configuration.environment, "fake-env1") + XCTAssertEqual(configuration.json?["test"].asString(), "value") XCTAssertNil(self.mockHTTP.lastRequestEndpoint) - expectation.fulfill() + } catch { + XCTFail("Should not fail") } - waitForExpectations(timeout: 1) } - func testGetConfig_performsGETWithCorrectPayload() { + func testGetConfig_performsGETWithCorrectPayload() async { mockHTTP.stubRequest(withMethod: "GET", toEndpoint: "/v1/configuration", respondWith: [] as [Any?], statusCode: 200) - - let expectation = expectation(description: "Callback invoked") - sut.getConfig { _, _ in + + do { + let _ = try await sut.getConfig() XCTAssertEqual(self.mockHTTP.lastRequestEndpoint, "v1/configuration") XCTAssertEqual(self.mockHTTP.lastRequestParameters?["configVersion"] as? String, "3") - expectation.fulfill() + } catch { + XCTFail("Should not fail") } - - waitForExpectations(timeout: 1) } - func testGetConfig_canGetRemoteConfiguration() { + func testGetConfig_canGetRemoteConfiguration() async { mockHTTP.cannedConfiguration = BTJSON(value: ["test": true]) mockHTTP.cannedStatusCode = 200 - - let expectation = expectation(description: "Fetch configuration") - sut.getConfig { configuration, error in + + do { + let configuration = try await sut.getConfig() XCTAssertNotNil(configuration) - XCTAssertNil(error) XCTAssertGreaterThanOrEqual(self.mockHTTP.GETRequestCount, 1) - - guard let json = configuration?.json else { return } + guard let json = configuration.json else { return } XCTAssertTrue(json["test"].isTrue) - expectation.fulfill() + } catch { + XCTFail("Should not fail") } - - waitForExpectations(timeout: 1) } - func testGetConfig_whenServerRespondsWithNon200StatusCode_returnsAPIClientError() { + func testGetConfig_whenServerRespondsWithNon200StatusCode_returnsAPIClientError() async { mockHTTP.stubRequest( withMethod: "GET", toEndpoint: "/client_api/v1/configuration", respondWith: ["error_message": "Something bad happened"], statusCode: 503 ) - - let expectation = expectation(description: "Callback invoked") - sut.getConfig { configuration, error in - guard let error = error as NSError? else { return } + + do { + let configuration = try await sut.getConfig() XCTAssertNil(configuration) + } catch { + guard let error = error as NSError? else { return } XCTAssertEqual(error.domain, BTAPIClientError.errorDomain) XCTAssertEqual(error.code, BTAPIClientError.configurationUnavailable.rawValue) XCTAssertEqual(error.localizedDescription, "The operation couldn’t be completed. Unable to fetch remote configuration from Braintree API at this time.") - expectation.fulfill() } - - waitForExpectations(timeout: 1) } - func testGetConfig_whenNetworkHasError_returnsNetworkErrorInCallback() { + func testGetConfig_whenNetworkHasError_returnsNetworkErrorInCallback() async { ConfigurationCache.shared.cacheInstance.removeAllObjects() let mockError: NSError = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost) mockHTTP.stubRequest(withMethod: "GET", toEndpoint: "/client_api/v1/configuration", respondWithError: mockError) - let expectation = expectation(description: "Fetch configuration") - sut.getConfig { configuration, error in + do { + let configuration = try await sut.getConfig() + XCTAssertNil(configuration) // BTAPIClient fetches the config when initialized so there can potentially be 2 requests here XCTAssertLessThanOrEqual(self.mockHTTP.GETRequestCount, 2) - XCTAssertNil(configuration) - XCTAssertEqual(error as NSError?, mockError) - expectation.fulfill() + } catch { + XCTFail("Should not fail") } - - waitForExpectations(timeout: 1) } - + func testGetConfig_returnsConfiguration() async throws { mockHTTP.cannedConfiguration = BTJSON(value: ["test": true]) mockHTTP.cannedStatusCode = 200 @@ -132,11 +124,15 @@ class ConfigurationLoader_Tests: XCTestCase { } } - func testGetConfig_whenCalledInQuickSequence_onlySendsOneNetworkRequest() { - sut.getConfig() { _, _ in } - sut.getConfig() { _, _ in } - sut.getConfig() { _, _ in } - sut.getConfig() { _, _ in } + func testGetConfig_whenCalledInQuickSequence_onlySendsOneNetworkRequest() async { + do { + let _ = try await sut.getConfig() + let _ = try await sut.getConfig() + let _ = try await sut.getConfig() + let _ = try await sut.getConfig() + } catch { + XCTFail("Should not fail") + } XCTAssertEqual(mockHTTP.GETRequestCount, 1) } diff --git a/UnitTests/BraintreeCoreTests/Configuration/MockConfigurationLoader.swift b/UnitTests/BraintreeCoreTests/Configuration/MockConfigurationLoader.swift index 27a9687541..b3ebcbfe92 100644 --- a/UnitTests/BraintreeCoreTests/Configuration/MockConfigurationLoader.swift +++ b/UnitTests/BraintreeCoreTests/Configuration/MockConfigurationLoader.swift @@ -12,21 +12,11 @@ class MockConfigurationLoader: ConfigurationLoader { super.init(http: http) } - override func getConfig(completion: @escaping (BTConfiguration?, Error?) -> Void) { - if let error = mockError { - completion(nil, error) - } else { - completion(mockConfig, nil) - } - } - override func getConfig() async throws -> BTConfiguration { if let error = mockError { throw error - } else if let config = mockConfig { - return config } else { - throw BTAPIClientError.configurationUnavailable + return BTConfiguration(json: nil) } } } From 5c7ae5cf2edc13e464f5a0aef996ea99097a0738 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 8 Aug 2024 08:54:12 -0500 Subject: [PATCH 07/15] run configuration fetch on main thread --- Sources/BraintreeCore/BTAPIClient.swift | 2 +- Sources/BraintreeCore/BTHTTP.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 92ddfce900..f6cf8efbb9 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -94,7 +94,7 @@ import Foundation @_documentation(visibility: private) public func fetchOrReturnRemoteConfiguration(_ completion: @escaping (BTConfiguration?, Error?) -> Void) { // TODO: - Consider updating all feature clients to use async version of this method? - Task { + Task { @MainActor in do { let configuration = try await configurationLoader.getConfig() setupHTTPCredentials(configuration) diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index e32e03756e..6aeb3f9aea 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -91,7 +91,6 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { continuation.resume(returning: (body ?? BTJSON(), response)) } } - // TODO: - How do we want to handle case of nil body or response } } From 29512261bea526b7b90ac5e145533d9aad8bbd5e Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 8 Aug 2024 09:06:09 -0500 Subject: [PATCH 08/15] update tests; cleanup logic --- Sources/BraintreeCore/BTHTTP.swift | 6 +++--- .../Configuration/ConfigurationLoader.swift | 7 ++----- .../Configuration/ConfigurationLoader_Tests.swift | 12 +++++------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index 6aeb3f9aea..13074d106d 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -82,13 +82,13 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { _ path: String, configuration: BTConfiguration? = nil, parameters: Encodable? = nil - ) async throws -> (BTJSON, HTTPURLResponse) { + ) async throws -> (BTJSON?, HTTPURLResponse?) { try await withCheckedThrowingContinuation { continuation in get(path, configuration: configuration, parameters: parameters) { body, response, error in if let error { continuation.resume(throwing: error) - } else if let response { - continuation.resume(returning: (body ?? BTJSON(), response)) + } else { + continuation.resume(returning: (body, response)) } } } diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 9321de0cc0..1ed1b9ba3f 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -42,13 +42,10 @@ class ConfigurationLoader { } while !isConfigCached { - print("While loop body") - do { - print("🤞GET request made") let (body, response) = try await http.get(configPath, parameters: BTConfigurationRequest()) - if response.statusCode != 200 { // || body == nil { + if response?.statusCode != 200 || body == nil { throw BTAPIClientError.configurationUnavailable } else { let configuration = BTConfiguration(json: body) @@ -64,7 +61,7 @@ class ConfigurationLoader { throw error } } - print("Exited while loop") + throw BTAPIClientError.configurationUnavailable } } diff --git a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift index b3accd0ad6..79079923da 100644 --- a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift @@ -70,8 +70,7 @@ class ConfigurationLoader_Tests: XCTestCase { ) do { - let configuration = try await sut.getConfig() - XCTAssertNil(configuration) + let _ = try await sut.getConfig() } catch { guard let error = error as NSError? else { return } XCTAssertEqual(error.domain, BTAPIClientError.errorDomain) @@ -86,12 +85,11 @@ class ConfigurationLoader_Tests: XCTestCase { mockHTTP.stubRequest(withMethod: "GET", toEndpoint: "/client_api/v1/configuration", respondWithError: mockError) do { - let configuration = try await sut.getConfig() - XCTAssertNil(configuration) + let _ = try await sut.getConfig() + } catch { // BTAPIClient fetches the config when initialized so there can potentially be 2 requests here XCTAssertLessThanOrEqual(self.mockHTTP.GETRequestCount, 2) - } catch { - XCTFail("Should not fail") + XCTAssertEqual(error as NSError?, mockError) } } @@ -131,7 +129,7 @@ class ConfigurationLoader_Tests: XCTestCase { let _ = try await sut.getConfig() let _ = try await sut.getConfig() } catch { - XCTFail("Should not fail") + // no op } XCTAssertEqual(mockHTTP.GETRequestCount, 1) From ecf97593b08b50c7a9d6c97576066ee68805e2c6 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 8 Aug 2024 09:46:17 -0500 Subject: [PATCH 09/15] combine task approach vs bool for config cache Co-authored-by: Victoria Park --- .../Features/ApplePayViewController.swift | 1 - Sources/BraintreeCore/BTAPIClient.swift | 1 + .../Configuration/ConfigurationLoader.swift | 23 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Demo/Application/Features/ApplePayViewController.swift b/Demo/Application/Features/ApplePayViewController.swift index 48e0499411..3c1bab9f7b 100644 --- a/Demo/Application/Features/ApplePayViewController.swift +++ b/Demo/Application/Features/ApplePayViewController.swift @@ -40,7 +40,6 @@ class ApplePayViewController: PaymentButtonBaseViewController { paymentAuthorizationViewController.delegate = self self.progressBlock("Presenting Apple Pay Sheet") - // TODO: why does this need dispatch queue main self.present(paymentAuthorizationViewController, animated: true) } } diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index f6cf8efbb9..16dae4c461 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -94,6 +94,7 @@ import Foundation @_documentation(visibility: private) public func fetchOrReturnRemoteConfiguration(_ completion: @escaping (BTConfiguration?, Error?) -> Void) { // TODO: - Consider updating all feature clients to use async version of this method? + Task { @MainActor in do { let configuration = try await configurationLoader.getConfig() diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 1ed1b9ba3f..ffba55090f 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -7,7 +7,8 @@ class ConfigurationLoader { private let configPath = "v1/configuration" private let configurationCache = ConfigurationCache.shared private let http: BTHTTP - private var isConfigCached = false // TODO: - Rename; should this bool live here? + + private var existingTask: Task? // MARK: - Initializer @@ -36,25 +37,24 @@ class ConfigurationLoader { /// - `Error?`: An error object if fetching the configuration fails or if the instance is deallocated. @_documentation(visibility: private) func getConfig() async throws -> BTConfiguration { + if let existingTask { + return try await existingTask.value + } + if let cachedConfig = try? configurationCache.getFromCache(authorization: http.authorization.bearer) { - isConfigCached = true return cachedConfig } - while !isConfigCached { + let task = Task { do { let (body, response) = try await http.get(configPath, parameters: BTConfigurationRequest()) - - if response?.statusCode != 200 || body == nil { + + // TODO: do we care if body is nil? + if response?.statusCode != 200 { throw BTAPIClientError.configurationUnavailable } else { let configuration = BTConfiguration(json: body) - try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration) - - NotificationCenter.default.post(name: Notification.Name("ConfigGet"), object: configuration) - isConfigCached = true - return configuration } } catch { @@ -62,6 +62,7 @@ class ConfigurationLoader { } } - throw BTAPIClientError.configurationUnavailable + existingTask = task + return try await task.value } } From cf9990cc76fd968160f10692a684637275665dc4 Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Thu, 8 Aug 2024 08:34:46 -0700 Subject: [PATCH 10/15] clear existingTask at end of Task block and add weak self --- .../BraintreeCore/Configuration/ConfigurationLoader.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index ffba55090f..3376afdca5 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -45,7 +45,12 @@ class ConfigurationLoader { return cachedConfig } - let task = Task { + let task = Task { [weak self] in + guard let self = self else { + throw BTAPIClientError.deallocated + } + + defer { self.existingTask = nil } do { let (body, response) = try await http.get(configPath, parameters: BTConfigurationRequest()) From cb3bf339093eac93e4d9b4d9466e73e729f7d1df Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 8 Aug 2024 10:46:58 -0500 Subject: [PATCH 11/15] address todo; update test --- .../BraintreeCore/Configuration/ConfigurationLoader.swift | 3 +-- .../Configuration/ConfigurationLoader_Tests.swift | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 3376afdca5..265c1bb3dc 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -54,8 +54,7 @@ class ConfigurationLoader { do { let (body, response) = try await http.get(configPath, parameters: BTConfigurationRequest()) - // TODO: do we care if body is nil? - if response?.statusCode != 200 { + if response?.statusCode != 200 || body == nil { throw BTAPIClientError.configurationUnavailable } else { let configuration = BTConfiguration(json: body) diff --git a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift index 79079923da..ac82e24cf8 100644 --- a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift @@ -39,11 +39,12 @@ class ConfigurationLoader_Tests: XCTestCase { do { let _ = try await sut.getConfig() - XCTAssertEqual(self.mockHTTP.lastRequestEndpoint, "v1/configuration") - XCTAssertEqual(self.mockHTTP.lastRequestParameters?["configVersion"] as? String, "3") } catch { - XCTFail("Should not fail") + // no-op } + + XCTAssertEqual(self.mockHTTP.lastRequestEndpoint, "v1/configuration") + XCTAssertEqual(self.mockHTTP.lastRequestParameters?["configVersion"] as? String, "3") } func testGetConfig_canGetRemoteConfiguration() async { From ad83d5c1fd64c257b4e6d0a500eb3cd47e4b75ff Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 8 Aug 2024 10:59:32 -0500 Subject: [PATCH 12/15] remove redundant self; check config first --- .../Configuration/ConfigurationLoader.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 265c1bb3dc..2fcda19fdb 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -37,16 +37,16 @@ class ConfigurationLoader { /// - `Error?`: An error object if fetching the configuration fails or if the instance is deallocated. @_documentation(visibility: private) func getConfig() async throws -> BTConfiguration { - if let existingTask { - return try await existingTask.value - } - if let cachedConfig = try? configurationCache.getFromCache(authorization: http.authorization.bearer) { return cachedConfig } + + if let existingTask { + return try await existingTask.value + } let task = Task { [weak self] in - guard let self = self else { + guard let self else { throw BTAPIClientError.deallocated } From da5da3778e795bf40611515dbb5880da884716b8 Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Thu, 8 Aug 2024 13:02:15 -0700 Subject: [PATCH 13/15] revert cache get/ existingTask check order swap for cache read/write safety --- .../BraintreeCore/Configuration/ConfigurationLoader.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 2fcda19fdb..183e18b0df 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -37,13 +37,13 @@ class ConfigurationLoader { /// - `Error?`: An error object if fetching the configuration fails or if the instance is deallocated. @_documentation(visibility: private) func getConfig() async throws -> BTConfiguration { - if let cachedConfig = try? configurationCache.getFromCache(authorization: http.authorization.bearer) { - return cachedConfig - } - if let existingTask { return try await existingTask.value } + + if let cachedConfig = try? configurationCache.getFromCache(authorization: http.authorization.bearer) { + return cachedConfig + } let task = Task { [weak self] in guard let self else { From 498b348e1c806e37650ca1bafaf49708d1909432 Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Fri, 9 Aug 2024 06:54:53 -0700 Subject: [PATCH 14/15] make tests parallel and add globalActor on getConfig --- .../Configuration/ConfigurationLoader.swift | 5 +++++ .../Configuration/ConfigurationLoader_Tests.swift | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift index 183e18b0df..bfa9212d8d 100644 --- a/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/Configuration/ConfigurationLoader.swift @@ -1,5 +1,9 @@ import Foundation +@globalActor actor ConfigurationActor { + static let shared = ConfigurationActor() +} + class ConfigurationLoader { // MARK: - Private Properties @@ -36,6 +40,7 @@ class ConfigurationLoader { /// - `BTConfiguration?`: The configuration object if it is successfully fetched or retrieved from the cache. /// - `Error?`: An error object if fetching the configuration fails or if the instance is deallocated. @_documentation(visibility: private) + @ConfigurationActor func getConfig() async throws -> BTConfiguration { if let existingTask { return try await existingTask.value diff --git a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift index ac82e24cf8..9bf0238d64 100644 --- a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift @@ -125,10 +125,11 @@ class ConfigurationLoader_Tests: XCTestCase { func testGetConfig_whenCalledInQuickSequence_onlySendsOneNetworkRequest() async { do { - let _ = try await sut.getConfig() - let _ = try await sut.getConfig() - let _ = try await sut.getConfig() - let _ = try await sut.getConfig() + async let functionOne = sut.getConfig() + async let two = sut.getConfig() + async let three = sut.getConfig() + async let four = sut.getConfig() + let _ = try await (functionOne, two, three, four) } catch { // no op } From 20d7a46d9381de1d6e01fd601c52f14fa668dbf5 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 9 Aug 2024 11:14:46 -0500 Subject: [PATCH 15/15] run fetchConfiguration for analytics on @MainActor --- Sources/BraintreeCore/BTAPIClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 16dae4c461..dd31f020d6 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -106,7 +106,7 @@ import Foundation } } - func fetchConfiguration() async throws -> BTConfiguration { + @MainActor func fetchConfiguration() async throws -> BTConfiguration { try await configurationLoader.getConfig() }