Skip to content

Commit

Permalink
Prevent duplicate v1/configuration GET requests (#1363)
Browse files Browse the repository at this point in the history
  • Loading branch information
scannillo authored Jul 16, 2024
1 parent 96a910d commit 10a0254
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 19 deletions.
4 changes: 4 additions & 0 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
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 */; };
Expand Down Expand Up @@ -855,6 +856,7 @@
8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsClient_Tests.swift; sourceTree = "<group>"; };
8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsRequest.swift; sourceTree = "<group>"; };
806C85622B90EBED00A2754C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
807296032C41B63F0093F2AB /* ConfigurationCallbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationCallbackStorage.swift; sourceTree = "<group>"; };
8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAPIRequest.swift; sourceTree = "<group>"; };
80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenizationKey_Tests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1433,6 +1435,7 @@
BEBD05212A1FE8BE0003C15C /* BTWebAuthenticationSession.swift */,
BEB9BF522A26872B00A3673E /* BTWebAuthenticationSessionClient.swift */,
BE698EA128AA8EEA001D9B10 /* ConfigurationCache.swift */,
807296032C41B63F0093F2AB /* ConfigurationCallbackStorage.swift */,
45EFC3962C2DBF32005E7F5B /* ConfigurationLoader.swift */,
80D280CC2BE9354B00762D27 /* Date+MillisecondTimestamp.swift */,
80F86FEF29FC2ED6003297FF /* Encodable+Dictionary.swift */,
Expand Down Expand Up @@ -3258,6 +3261,7 @@
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 */,
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Braintree iOS SDK Release Notes

## unreleased
* Prevent duplicate outbound `v1/configuration` requests

## 6.23.0 (2024-07-15)
* BraintreeShopperInsights (BETA)
* Add error when using an invalid authorization type
Expand Down
26 changes: 26 additions & 0 deletions Sources/BraintreeCore/ConfigurationCallbackStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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 {

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 }
}

/// Adds a pending, yet to-be-invoked completion handler
func add(_ completion: @escaping (BTConfiguration?, Error?) -> Void) {
queue.sync { 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()
}
}
}
51 changes: 32 additions & 19 deletions Sources/BraintreeCore/ConfigurationLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class ConfigurationLoader {
private let configPath = "v1/configuration"
private let configurationCache = ConfigurationCache.shared
private let http: BTHTTP
private let pendingCompletions = ConfigurationCallbackStorage()

// MARK: - Intitializer

Expand Down Expand Up @@ -39,26 +40,32 @@ class ConfigurationLoader {
completion(cachedConfig, nil)
return
}

http.get(configPath, parameters: BTConfigurationRequest()) { [weak self] body, response, error in
guard let self else {
completion(nil, BTAPIClientError.deallocated)
return
}

if let error {
completion(nil, error)
return
} else if response?.statusCode != 200 || body == nil {
completion(nil, BTAPIClientError.configurationUnavailable)
return
} else {
let configuration = BTConfiguration(json: body)

try? configurationCache.putInCache(authorization: http.authorization.bearer, configuration: configuration)

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
}

completion(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
}
}
}
}
Expand All @@ -74,4 +81,10 @@ class ConfigurationLoader {
}
}
}

// MARK: - Private Methods

func notifyCompletions(_ configuration: BTConfiguration?, _ error: Error?) {
pendingCompletions.invoke(configuration, error)
}
}
9 changes: 9 additions & 0 deletions UnitTests/BraintreeCoreTests/ConfigurationLoader_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,13 @@ class ConfigurationLoader_Tests: XCTestCase {
XCTAssertEqual(error as NSError, mockError)
}
}

func testGetConfig_whenCalledInQuickSequence_onlySendsOneNetworkRequest() {
sut.getConfig() { _, _ in }
sut.getConfig() { _, _ in }
sut.getConfig() { _, _ in }
sut.getConfig() { _, _ in }

XCTAssertEqual(mockHTTP.GETRequestCount, 1)
}
}

0 comments on commit 10a0254

Please sign in to comment.