From 2726050dc60b16b064fab9900b9346467da66e50 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 19 Feb 2025 16:40:04 -0600 Subject: [PATCH 01/14] WIP - update to handle invalid autorization --- Braintree.xcodeproj/project.pbxproj | 4 ++ .../PaymentButtonBaseViewController.swift | 2 + .../Features/AmexViewController.swift | 2 +- .../BTAmericanExpressClient.swift | 6 +- .../Authorization/ClientAuthorization.swift | 1 + .../Authorization/InvalidAuthorization.swift | 16 +++++ .../Authorization/TokenizationKey.swift | 1 + Sources/BraintreeCore/BTAPIClient.swift | 72 +++++++++++++++++-- Sources/BraintreeCore/BTHTTPError.swift | 7 ++ 9 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 Sources/BraintreeCore/Authorization/InvalidAuthorization.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index c342622342..de0d8cdd89 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -272,6 +272,7 @@ BE80C00D29C8B4B900793A6C /* BTCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE80C00C29C8B4B900793A6C /* BTCard.swift */; }; BE82E73B29C49C050059FE97 /* BTThreeDSecureV2ButtonCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE82E73A29C49C050059FE97 /* BTThreeDSecureV2ButtonCustomization.swift */; }; BE82E73F29C4A06B0059FE97 /* BTThreeDSecureV2ToolbarCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE82E73E29C4A06B0059FE97 /* BTThreeDSecureV2ToolbarCustomization.swift */; }; + BE849EED2D66908E005BD215 /* InvalidAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE849EEC2D669088005BD215 /* InvalidAuthorization.swift */; }; BE895C61299433FB008112AB /* BTApplePayCardNonce.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE895C60299433FB008112AB /* BTApplePayCardNonce.swift */; }; BE895C6329944BD3008112AB /* BTApplePayError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE895C6229944BD3008112AB /* BTApplePayError.swift */; }; BE895C6529944BF5008112AB /* BTApplePayClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE895C6429944BF5008112AB /* BTApplePayClient.swift */; }; @@ -966,6 +967,7 @@ BE82E73C29C49ECE0059FE97 /* BTThreeDSecureV2LabelCustomization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureV2LabelCustomization.swift; sourceTree = ""; }; BE82E73E29C4A06B0059FE97 /* BTThreeDSecureV2ToolbarCustomization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureV2ToolbarCustomization.swift; sourceTree = ""; }; BE82E74029C4A1330059FE97 /* BTThreeDSecureV2TextBoxCustomization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureV2TextBoxCustomization.swift; sourceTree = ""; }; + BE849EEC2D669088005BD215 /* InvalidAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidAuthorization.swift; sourceTree = ""; }; BE895C60299433FB008112AB /* BTApplePayCardNonce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce.swift; sourceTree = ""; }; BE895C6229944BD3008112AB /* BTApplePayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayError.swift; sourceTree = ""; }; BE895C6429944BF5008112AB /* BTApplePayClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayClient.swift; sourceTree = ""; }; @@ -1583,6 +1585,7 @@ 80B207342BF6D3B000787E37 /* Authorization */ = { isa = PBXGroup; children = ( + BE849EEC2D669088005BD215 /* InvalidAuthorization.swift */, BED00CAF28A579D700D74AEC /* BTClientToken.swift */, BED00CB128A57AD400D74AEC /* BTClientTokenError.swift */, BE24C67428E7491E0067B11A /* ClientAuthorization.swift */, @@ -3207,6 +3210,7 @@ BE2F98D028A2BCCD008EF189 /* BTConfiguration.swift in Sources */, 804DC45D2B2D08FF00F17A15 /* BTConfigurationRequest.swift in Sources */, BED00CB228A57AD400D74AEC /* BTClientTokenError.swift in Sources */, + BE849EED2D66908E005BD215 /* InvalidAuthorization.swift in Sources */, BE24C67328E73E810067B11A /* BTAPIClientHTTPType.swift in Sources */, 457D7FC82C29CEC300EF6523 /* RepeatingTimer.swift in Sources */, BE9EC0982899CF040022EC63 /* BTAPIPinnedCertificates.swift in Sources */, diff --git a/Demo/Application/Base/PaymentButtonBaseViewController.swift b/Demo/Application/Base/PaymentButtonBaseViewController.swift index 3c1456fc2b..cd6ca89109 100644 --- a/Demo/Application/Base/PaymentButtonBaseViewController.swift +++ b/Demo/Application/Base/PaymentButtonBaseViewController.swift @@ -4,6 +4,7 @@ import BraintreeCore class PaymentButtonBaseViewController: BaseViewController { let apiClient: BTAPIClient + let authorization: String var heightConstraint: CGFloat? @@ -12,6 +13,7 @@ class PaymentButtonBaseViewController: BaseViewController { override init(authorization: String) { // swiftlint:disable:next force_unwrapping apiClient = BTAPIClient(authorization: authorization)! + self.authorization = authorization super.init(authorization: authorization) } diff --git a/Demo/Application/Features/AmexViewController.swift b/Demo/Application/Features/AmexViewController.swift index 1c05997905..e2d94c2fba 100644 --- a/Demo/Application/Features/AmexViewController.swift +++ b/Demo/Application/Features/AmexViewController.swift @@ -4,7 +4,7 @@ import BraintreeCard class AmexViewController: PaymentButtonBaseViewController { - lazy var amexClient = BTAmericanExpressClient(apiClient: apiClient) + lazy var amexClient = BTAmericanExpressClient(authorization: authorization) lazy var cardClient = BTCardClient(apiClient: apiClient) override func viewDidLoad() { diff --git a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift index fe3c2ca58f..fb88aaba35 100644 --- a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift +++ b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift @@ -10,10 +10,10 @@ import BraintreeCore private let apiClient: BTAPIClient /// Creates an American Express client. - /// - Parameter apiClient: An instance of `BTAPIClient` + /// - Parameter authorization: A client token or tokenization key @objc(initWithAPIClient:) - public init(apiClient: BTAPIClient) { - self.apiClient = apiClient + public init(authorization: String) { + self.apiClient = BTAPIClient(newAuthorization: authorization) } /// Gets the rewards balance associated with a Braintree nonce. Only for American Express cards. diff --git a/Sources/BraintreeCore/Authorization/ClientAuthorization.swift b/Sources/BraintreeCore/Authorization/ClientAuthorization.swift index 1b5d0d594f..d4b8fc93ce 100644 --- a/Sources/BraintreeCore/Authorization/ClientAuthorization.swift +++ b/Sources/BraintreeCore/Authorization/ClientAuthorization.swift @@ -24,4 +24,5 @@ public protocol ClientAuthorization { public enum AuthorizationType { case tokenizationKey case clientToken + case invalidAuthorization } diff --git a/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift new file mode 100644 index 0000000000..be58999720 --- /dev/null +++ b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift @@ -0,0 +1,16 @@ +import Foundation + +/// An invalid authorization type +class InvalidAuthorization: ClientAuthorization { + + let type = AuthorizationType.invalidAuthorization + let configURL: URL + let bearer: String + let originalValue: String + + init(_ rawValue: String) { + self.bearer = rawValue + self.originalValue = rawValue + self.configURL = URL(string: "https://example.com")! + } +} diff --git a/Sources/BraintreeCore/Authorization/TokenizationKey.swift b/Sources/BraintreeCore/Authorization/TokenizationKey.swift index e291045b47..8aebe3492d 100644 --- a/Sources/BraintreeCore/Authorization/TokenizationKey.swift +++ b/Sources/BraintreeCore/Authorization/TokenizationKey.swift @@ -17,6 +17,7 @@ class TokenizationKey: ClientAuthorization { guard let configURL = TokenizationKey.baseURLFromTokenizationKey(rawValue) else { throw TokenizationKeyError.invalid } + self.configURL = configURL } diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index ecdf34656f..5094b4db2d 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -9,10 +9,10 @@ import Foundation public typealias RequestCompletion = (BTJSON?, HTTPURLResponse?, Error?) -> Void // MARK: - Public Properties - + /// The TokenizationKey or ClientToken used to authorize the APIClient public var authorization: ClientAuthorization - + /// Client metadata that is used for tracking the client session public private(set) var metadata: BTClientMetadata @@ -34,7 +34,7 @@ import Foundation public init?(authorization: String) { self.metadata = BTClientMetadata() - guard let authorizationType = Self.authorizationType(for: authorization) else { return nil } + let authorizationType = Self.authorizationType(for: authorization) switch authorizationType { case .tokenizationKey: @@ -50,6 +50,8 @@ import Foundation } catch { return nil } + case .invalidAuthorization: + return nil } let btHttp = BTHTTP(authorization: self.authorization) @@ -65,6 +67,38 @@ import Foundation // No-op } } + + /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. + /// Initialize a new API client. + /// - Parameter authorization: Your tokenization key or client token. + // TODO: remove obj-c init once we can remove the old init + // TODO: remove default/optional nil, needed currently because otherwise there is and error that the signatures are the same + // TODO: rename param to authorization in final PR + @_documentation(visibility: private) + @objc(initWithAuthorizationNew:) + public convenience init(newAuthorization: String? = nil) { + + // TODO: this will not be force unwrapped once we remove the other init + self.init(authorization: newAuthorization ?? "")! + + self.metadata = BTClientMetadata() + + // TODO: remove default and enforce authorization above + self.authorization = self.authorization(from: newAuthorization ?? "") + + let btHttp = BTHTTP(authorization: self.authorization) + http = btHttp + configurationLoader = ConfigurationLoader(http: btHttp) + + analyticsService.setAPIClient(self) + http?.networkTimingDelegate = self + + // Kickoff the background request to fetch the config + fetchOrReturnRemoteConfiguration { _, _ in + // No-op + } + } + // MARK: - Deinit @@ -291,11 +325,11 @@ import Foundation // MARK: - Internal Static Methods - static func authorizationType(for authorization: String) -> AuthorizationType? { + static func authorizationType(for authorization: String) -> AuthorizationType { let pattern: String = "([a-zA-Z0-9]+)_[a-zA-Z0-9]+_([a-zA-Z0-9_]+)" - guard let regularExpression = try? NSRegularExpression(pattern: pattern) else { return nil } + let regularExpression = try? NSRegularExpression(pattern: pattern) - let tokenizationKeyMatch: NSTextCheckingResult? = regularExpression.firstMatch( + let tokenizationKeyMatch: NSTextCheckingResult? = regularExpression?.firstMatch( in: authorization, options: [], range: NSRange(location: 0, length: authorization.count) @@ -335,7 +369,7 @@ import Foundation } } - func payPalAPIURL(forEnvironment environment: String) -> URL? { + private func payPalAPIURL(forEnvironment environment: String) -> URL? { if environment.caseInsensitiveCompare("sandbox") == .orderedSame || environment.caseInsensitiveCompare("development") == .orderedSame { return BTCoreConstants.payPalSandboxURL @@ -343,6 +377,30 @@ import Foundation return BTCoreConstants.payPalProductionURL } } + + private func authorization(from authorization: String) -> ClientAuthorization { + let authorizationType = Self.authorizationType(for: authorization) + + switch authorizationType { + case .tokenizationKey: + do { + return try TokenizationKey(authorization) + } catch { + return InvalidAuthorization(authorization) + } + case .clientToken: + do { + let clientToken = try BTClientToken(clientToken: authorization) + self.authorization = clientToken + } catch { + return InvalidAuthorization(authorization) + } + case .invalidAuthorization: + return InvalidAuthorization(authorization) + } + + return InvalidAuthorization(authorization) + } // MARK: BTAPITimingDelegate conformance diff --git a/Sources/BraintreeCore/BTHTTPError.swift b/Sources/BraintreeCore/BTHTTPError.swift index dedda6c485..6fca42bafa 100644 --- a/Sources/BraintreeCore/BTHTTPError.swift +++ b/Sources/BraintreeCore/BTHTTPError.swift @@ -41,6 +41,9 @@ public enum BTHTTPError: Error, CustomNSError, LocalizedError, Equatable { /// 12. Deallocated HTTPClient case deallocated(String) + + /// 13. Invalid authorization + case invalidAuthorization(String) public static var errorDomain: String { BTCoreConstants.httpErrorDomain @@ -74,6 +77,8 @@ public enum BTHTTPError: Error, CustomNSError, LocalizedError, Equatable { return 11 case .deallocated: return 12 + case .invalidAuthorization: + return 13 } } @@ -105,6 +110,8 @@ public enum BTHTTPError: Error, CustomNSError, LocalizedError, Equatable { return [NSLocalizedDescriptionKey: errorDescription] case .deallocated(let httpType): return [NSLocalizedDescriptionKey: "\(httpType) has been deallocated."] + case .invalidAuthorization(let authorization): + return [NSLocalizedDescriptionKey: "Invalid authorization provided: \(authorization)."] } } From 6b6e23b25df6369959d51c6751a30dac3185f7ff Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 09:21:14 -0600 Subject: [PATCH 02/14] add new invalidAuthorization error --- Sources/BraintreeCore/BTAPIClientError.swift | 22 ++++++++++++++++++-- Sources/BraintreeCore/BTHTTPError.swift | 7 ------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClientError.swift b/Sources/BraintreeCore/BTAPIClientError.swift index dffb34f80a..ef0ce9ea60 100644 --- a/Sources/BraintreeCore/BTAPIClientError.swift +++ b/Sources/BraintreeCore/BTAPIClientError.swift @@ -1,7 +1,7 @@ import Foundation /// Error codes associated with a API Client. -public enum BTAPIClientError: Int, Error, CustomNSError, LocalizedError, Equatable { +public enum BTAPIClientError: Error, CustomNSError, LocalizedError, Equatable { /// 0. Configuration fetch failed case configurationUnavailable @@ -14,13 +14,27 @@ public enum BTAPIClientError: Int, Error, CustomNSError, LocalizedError, Equatab /// 3. Failed to base64 encode an authorizationFingerprint or tokenizationKey, when used as a cacheKey case failedBase64Encoding + + /// 4. Invalid authorization + case invalidAuthorization(String) public static var errorDomain: String { "com.braintreepayments.BTAPIClientErrorDomain" } public var errorCode: Int { - rawValue + switch self { + case .configurationUnavailable: + return 0 + case .notAuthorized: + return 1 + case .deallocated: + return 2 + case .failedBase64Encoding: + return 3 + case .invalidAuthorization: + return 4 + } } public var errorDescription: String? { @@ -36,6 +50,10 @@ public enum BTAPIClientError: Int, Error, CustomNSError, LocalizedError, Equatab case .failedBase64Encoding: return "Unable to base64 encode the authorization string." + + case .invalidAuthorization(let authorization): + return "Invalid authorization provided: \(authorization)." + } } } diff --git a/Sources/BraintreeCore/BTHTTPError.swift b/Sources/BraintreeCore/BTHTTPError.swift index 6fca42bafa..dedda6c485 100644 --- a/Sources/BraintreeCore/BTHTTPError.swift +++ b/Sources/BraintreeCore/BTHTTPError.swift @@ -41,9 +41,6 @@ public enum BTHTTPError: Error, CustomNSError, LocalizedError, Equatable { /// 12. Deallocated HTTPClient case deallocated(String) - - /// 13. Invalid authorization - case invalidAuthorization(String) public static var errorDomain: String { BTCoreConstants.httpErrorDomain @@ -77,8 +74,6 @@ public enum BTHTTPError: Error, CustomNSError, LocalizedError, Equatable { return 11 case .deallocated: return 12 - case .invalidAuthorization: - return 13 } } @@ -110,8 +105,6 @@ public enum BTHTTPError: Error, CustomNSError, LocalizedError, Equatable { return [NSLocalizedDescriptionKey: errorDescription] case .deallocated(let httpType): return [NSLocalizedDescriptionKey: "\(httpType) has been deallocated."] - case .invalidAuthorization(let authorization): - return [NSLocalizedDescriptionKey: "Invalid authorization provided: \(authorization)."] } } From 863775271223dddf3ef8fdbb35c9d43128399696 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 09:21:32 -0600 Subject: [PATCH 03/14] update logic to check for invalid authorization --- Sources/BraintreeCore/BTAPIClient.swift | 39 ++++++++++++++----------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 5094b4db2d..9e312b0eeb 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -67,13 +67,13 @@ import Foundation // No-op } } - - /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. - /// Initialize a new API client. - /// - Parameter authorization: Your tokenization key or client token. + // TODO: remove obj-c init once we can remove the old init // TODO: remove default/optional nil, needed currently because otherwise there is and error that the signatures are the same // TODO: rename param to authorization in final PR + /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. + /// Initialize a new API client. + /// - Parameter authorization: Your tokenization key or client token. @_documentation(visibility: private) @objc(initWithAuthorizationNew:) public convenience init(newAuthorization: String? = nil) { @@ -127,20 +127,28 @@ 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() - setupHTTPCredentials(configuration) - completion(configuration, nil) - } catch { - completion(nil, error) + + if authorization.type == .invalidAuthorization { + completion(nil, BTAPIClientError.invalidAuthorization(authorization.originalValue)) + } else { + Task { @MainActor in + do { + let configuration = try await configurationLoader.getConfig() + setupHTTPCredentials(configuration) + completion(configuration, nil) + } catch { + completion(nil, error) + } } } } @MainActor func fetchConfiguration() async throws -> BTConfiguration { - try await configurationLoader.getConfig() + if authorization.type == .invalidAuthorization { + throw BTAPIClientError.invalidAuthorization(authorization.originalValue) + } else { + try await configurationLoader.getConfig() + } } /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. @@ -390,16 +398,13 @@ import Foundation } case .clientToken: do { - let clientToken = try BTClientToken(clientToken: authorization) - self.authorization = clientToken + return try BTClientToken(clientToken: authorization) } catch { return InvalidAuthorization(authorization) } case .invalidAuthorization: return InvalidAuthorization(authorization) } - - return InvalidAuthorization(authorization) } // MARK: BTAPITimingDelegate conformance From 1f41cade797daa73fcc40e74eb4926e5a2f50e5a Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 09:58:51 -0600 Subject: [PATCH 04/14] update unit tests --- ...raintreeAmexExpress_IntegrationTests.swift | 2 +- .../BTAmericanExpressClient.swift | 3 +- Sources/BraintreeCore/BTAPIClient.swift | 17 ++++------- .../BTAmericanExpressClient_Tests.swift | 29 +++++++++++++++++-- .../ConfigurationLoader_Tests.swift | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/IntegrationTests/BraintreeAmexExpress_IntegrationTests.swift b/IntegrationTests/BraintreeAmexExpress_IntegrationTests.swift index 2e5f523677..57c031546e 100644 --- a/IntegrationTests/BraintreeAmexExpress_IntegrationTests.swift +++ b/IntegrationTests/BraintreeAmexExpress_IntegrationTests.swift @@ -8,7 +8,7 @@ class BraintreeAmexExpress_IntegrationTests: XCTestCase { func testGetRewardsBalance_returnsResult() async { let apiClient = BTAPIClient(authorization: BTIntegrationTestsConstants.sandboxClientTokenVersion3)! let cardClient = BTCardClient(apiClient: apiClient) - let amexClient = BTAmericanExpressClient(apiClient: apiClient) + let amexClient = BTAmericanExpressClient(authorization: BTIntegrationTestsConstants.sandboxClientTokenVersion3) let card = BTCard( number: "371260714673002", diff --git a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift index fb88aaba35..31d9bb84df 100644 --- a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift +++ b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift @@ -7,7 +7,8 @@ import BraintreeCore /// `BTAmericanExpressClient` enables you to look up the rewards balance of American Express cards. @objc public class BTAmericanExpressClient: NSObject { - private let apiClient: BTAPIClient + /// exposed for testing + var apiClient: BTAPIClient /// Creates an American Express client. /// - Parameter authorization: A client token or tokenization key diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 9e312b0eeb..49917eef83 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -69,27 +69,22 @@ import Foundation } // TODO: remove obj-c init once we can remove the old init - // TODO: remove default/optional nil, needed currently because otherwise there is and error that the signatures are the same // TODO: rename param to authorization in final PR /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. /// Initialize a new API client. /// - Parameter authorization: Your tokenization key or client token. @_documentation(visibility: private) @objc(initWithAuthorizationNew:) - public convenience init(newAuthorization: String? = nil) { - - // TODO: this will not be force unwrapped once we remove the other init - self.init(authorization: newAuthorization ?? "")! - + public init(newAuthorization: String) { + self.authorization = Self.authorization(from: newAuthorization) self.metadata = BTClientMetadata() - - // TODO: remove default and enforce authorization above - self.authorization = self.authorization(from: newAuthorization ?? "") - + let btHttp = BTHTTP(authorization: self.authorization) http = btHttp configurationLoader = ConfigurationLoader(http: btHttp) + super.init() + analyticsService.setAPIClient(self) http?.networkTimingDelegate = self @@ -386,7 +381,7 @@ import Foundation } } - private func authorization(from authorization: String) -> ClientAuthorization { + private static func authorization(from authorization: String) -> ClientAuthorization { let authorizationType = Self.authorizationType(for: authorization) switch authorizationType { diff --git a/UnitTests/BraintreeAmericanExpressTests/BTAmericanExpressClient_Tests.swift b/UnitTests/BraintreeAmericanExpressTests/BTAmericanExpressClient_Tests.swift index f55e284a72..972068c861 100644 --- a/UnitTests/BraintreeAmericanExpressTests/BTAmericanExpressClient_Tests.swift +++ b/UnitTests/BraintreeAmericanExpressTests/BTAmericanExpressClient_Tests.swift @@ -10,12 +10,12 @@ class BTAmericanExpressClient_Tests: XCTestCase { override func setUp() { super.setUp() - mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! - amexClient = BTAmericanExpressClient(apiClient: mockAPIClient) + amexClient = BTAmericanExpressClient(authorization: "development_tokenization_key") + amexClient?.apiClient = mockAPIClient } func testGetRewardsBalance_formatsGETRequest() async { - let result = try? await amexClient!.getRewardsBalance(forNonce: "fake-nonce", currencyISOCode: "fake-code") + let _ = try? await amexClient!.getRewardsBalance(forNonce: "fake-nonce", currencyISOCode: "fake-code") XCTAssertEqual(mockAPIClient.lastGETPath, "v1/payment_methods/amex_rewards_balance") @@ -91,4 +91,27 @@ class BTAmericanExpressClient_Tests: XCTestCase { XCTAssertEqual(mockAPIClient.postedAnalyticsEvents[mockAPIClient.postedAnalyticsEvents.count - 2], "amex:rewards-balance:started") XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "amex:rewards-balance:failed") } + + func testGetRewardsBalance_withInvalidAuthorization_returnsError() { + amexClient = BTAmericanExpressClient(authorization: "badAuth") + mockAPIClient.cannedResponseError = NSError( + domain: BTAPIClientError.errorDomain, + code: BTAPIClientError.invalidAuthorization("").errorCode, + userInfo: [NSLocalizedDescriptionKey: BTAPIClientError.invalidAuthorization("").errorDescription ?? ""] + ) + + let expectation = expectation(description: "Amex reward balance should return invalid authorization error") + amexClient?.getRewardsBalance(forNonce: "", currencyISOCode: "") { rewardsBalance, error in + XCTAssertNil(rewardsBalance) + if let error = error as NSError? { + XCTAssertEqual(error.code, BTAPIClientError.invalidAuthorization("").errorCode) + XCTAssertEqual(error.localizedDescription, "Invalid authorization provided: badAuth.") + XCTAssertEqual(error.domain, BTAPIClientError.errorDomain) + } + + expectation.fulfill() + } + + waitForExpectations(timeout: 2) + } } diff --git a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift index 9bf0238d64..b938f44d24 100644 --- a/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Configuration/ConfigurationLoader_Tests.swift @@ -75,7 +75,7 @@ class ConfigurationLoader_Tests: XCTestCase { } catch { guard let error = error as NSError? else { return } XCTAssertEqual(error.domain, BTAPIClientError.errorDomain) - XCTAssertEqual(error.code, BTAPIClientError.configurationUnavailable.rawValue) + XCTAssertEqual(error.code, BTAPIClientError.configurationUnavailable.errorCode) XCTAssertEqual(error.localizedDescription, "The operation couldn’t be completed. Unable to fetch remote configuration from Braintree API at this time.") } } From eaa1e048ddca0815e9b0e2c31a3ddf418339fee8 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 10:18:29 -0600 Subject: [PATCH 05/14] cleanup logic; update docstrings --- .../PaymentButtonBaseViewController.swift | 1 + .../BTAmericanExpressClient.swift | 2 +- Sources/BraintreeCore/BTAPIClient.swift | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Demo/Application/Base/PaymentButtonBaseViewController.swift b/Demo/Application/Base/PaymentButtonBaseViewController.swift index cd6ca89109..ec267a4bb3 100644 --- a/Demo/Application/Base/PaymentButtonBaseViewController.swift +++ b/Demo/Application/Base/PaymentButtonBaseViewController.swift @@ -3,6 +3,7 @@ import BraintreeCore class PaymentButtonBaseViewController: BaseViewController { + // TODO: remove API client in final PR let apiClient: BTAPIClient let authorization: String diff --git a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift index 31d9bb84df..fe74e8e9d9 100644 --- a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift +++ b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift @@ -11,7 +11,7 @@ import BraintreeCore var apiClient: BTAPIClient /// Creates an American Express client. - /// - Parameter authorization: A client token or tokenization key + /// - Parameter authorization: Your client token or tokenization key @objc(initWithAPIClient:) public init(authorization: String) { self.apiClient = BTAPIClient(newAuthorization: authorization) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 49917eef83..6e89f094bf 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -1,5 +1,6 @@ import Foundation +// swiftlint:disable type_body_length /// This class acts as the entry point for accessing the Braintree APIs via common HTTP methods performed on API endpoints. /// - Note: It also manages authentication via tokenization key and provides access to a merchant's gateway configuration. @objcMembers public class BTAPIClient: NSObject, BTHTTPNetworkTiming { @@ -9,7 +10,7 @@ import Foundation public typealias RequestCompletion = (BTJSON?, HTTPURLResponse?, Error?) -> Void // MARK: - Public Properties - + /// The TokenizationKey or ClientToken used to authorize the APIClient public var authorization: ClientAuthorization @@ -68,20 +69,18 @@ import Foundation } } - // TODO: remove obj-c init once we can remove the old init - // TODO: rename param to authorization in final PR + // TODO: rename param to authorization in final PR - set as newAuthorization currently since otherwise the two inits have the same signature /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. /// Initialize a new API client. /// - Parameter authorization: Your tokenization key or client token. @_documentation(visibility: private) - @objc(initWithAuthorizationNew:) public init(newAuthorization: String) { self.authorization = Self.authorization(from: newAuthorization) self.metadata = BTClientMetadata() - let btHttp = BTHTTP(authorization: self.authorization) - http = btHttp - configurationLoader = ConfigurationLoader(http: btHttp) + let btHTTP = BTHTTP(authorization: self.authorization) + http = btHTTP + configurationLoader = ConfigurationLoader(http: btHTTP) super.init() @@ -330,9 +329,11 @@ import Foundation static func authorizationType(for authorization: String) -> AuthorizationType { let pattern: String = "([a-zA-Z0-9]+)_[a-zA-Z0-9]+_([a-zA-Z0-9_]+)" - let regularExpression = try? NSRegularExpression(pattern: pattern) + guard let regularExpression = try? NSRegularExpression(pattern: pattern) else { + return .invalidAuthorization + } - let tokenizationKeyMatch: NSTextCheckingResult? = regularExpression?.firstMatch( + let tokenizationKeyMatch: NSTextCheckingResult? = regularExpression.firstMatch( in: authorization, options: [], range: NSRange(location: 0, length: authorization.count) From 1eb4fea55845ef11668548477016320f271350cb Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 10:45:24 -0600 Subject: [PATCH 06/14] add migration guide and changelog entry --- CHANGELOG.md | 2 ++ V7_MIGRATION.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a6116ba4..74ee070d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ * Remove `fetchPaymentMethodNonces` methods and parser * BraintreePayPal * Update PayPal app URL query scheme from `paypal-app-switch-checkout` to `paypal` + * BraintreeAmericanExpress + * Update initalizer to `BTAmericanExpressClient(authorization:)` ## 6.27.0 (2025-01-23) * BraintreePayPal diff --git a/V7_MIGRATION.md b/V7_MIGRATION.md index 6a3e212135..72ae036583 100644 --- a/V7_MIGRATION.md +++ b/V7_MIGRATION.md @@ -14,6 +14,7 @@ _Documentation for v7 will be published to https://developer.paypal.com/braintre 1. [3D Secure](#3d-secure)] 1. [PayPal](#paypal) 1. [PayPal Native Checkout](#paypal-native-checkout) +1. [American Express](#american-express) ## Supported Versions @@ -68,3 +69,10 @@ For the App Switch flow, you must update your `info.plist` with a simplified URL ## PayPal Native Checkout The PayPal Native Checkout integration is no longer supported. Please remove it from your app and use the [PayPal (web)](https://developer.paypal.com/braintree/docs/guides/paypal/overview/ios/v6) integration. + +## American Express +Update initazlier for `BTAmericanExpressClient`: +```diff +- var amexClient = BTAmericanExpressClient(apiClient: apiClient) ++ var amexClient = BTAmericanExpressClient(authorization: authorization) +``` From cca4160bacfaa34f6bed589094ad1d557375669b Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 10:49:54 -0600 Subject: [PATCH 07/14] fix type --- CHANGELOG.md | 2 +- V7_MIGRATION.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ee070d1f..28dd9c06b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ * BraintreePayPal * Update PayPal app URL query scheme from `paypal-app-switch-checkout` to `paypal` * BraintreeAmericanExpress - * Update initalizer to `BTAmericanExpressClient(authorization:)` + * Update initializer to `BTAmericanExpressClient(authorization:)` ## 6.27.0 (2025-01-23) * BraintreePayPal diff --git a/V7_MIGRATION.md b/V7_MIGRATION.md index 72ae036583..3ae44166d3 100644 --- a/V7_MIGRATION.md +++ b/V7_MIGRATION.md @@ -71,7 +71,7 @@ The PayPal Native Checkout integration is no longer supported. Please remove it use the [PayPal (web)](https://developer.paypal.com/braintree/docs/guides/paypal/overview/ios/v6) integration. ## American Express -Update initazlier for `BTAmericanExpressClient`: +Update initializer for `BTAmericanExpressClient`: ```diff - var amexClient = BTAmericanExpressClient(apiClient: apiClient) + var amexClient = BTAmericanExpressClient(authorization: authorization) From 370bb5844705cf9895b3590699be0c7ad653d681 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 11:14:07 -0600 Subject: [PATCH 08/14] update linter warnings --- Sources/BraintreeCore/Authorization/InvalidAuthorization.swift | 2 ++ Sources/BraintreeCore/BTAPIClient.swift | 2 +- Sources/BraintreeCore/BTAPIClientError.swift | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift index be58999720..b47a902ad2 100644 --- a/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift +++ b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift @@ -11,6 +11,8 @@ class InvalidAuthorization: ClientAuthorization { init(_ rawValue: String) { self.bearer = rawValue self.originalValue = rawValue + + // swiftlint:disable:next force_unwrapping self.configURL = URL(string: "https://example.com")! } } diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 6e89f094bf..dc440cda3b 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -1,6 +1,6 @@ import Foundation -// swiftlint:disable type_body_length +// swiftlint:disable type_body_length file_length /// This class acts as the entry point for accessing the Braintree APIs via common HTTP methods performed on API endpoints. /// - Note: It also manages authentication via tokenization key and provides access to a merchant's gateway configuration. @objcMembers public class BTAPIClient: NSObject, BTHTTPNetworkTiming { diff --git a/Sources/BraintreeCore/BTAPIClientError.swift b/Sources/BraintreeCore/BTAPIClientError.swift index ef0ce9ea60..74a8f1d255 100644 --- a/Sources/BraintreeCore/BTAPIClientError.swift +++ b/Sources/BraintreeCore/BTAPIClientError.swift @@ -53,7 +53,6 @@ public enum BTAPIClientError: Error, CustomNSError, LocalizedError, Equatable { case .invalidAuthorization(let authorization): return "Invalid authorization provided: \(authorization)." - } } } From 1159d5b46307da4d8e2f9d111b2620ee59ff2a37 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 20 Feb 2025 11:14:14 -0600 Subject: [PATCH 09/14] update sample apps --- SampleApps/CarthageTest/CarthageTest/ViewController.swift | 2 +- SampleApps/SPMTest/SPMTest/ViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SampleApps/CarthageTest/CarthageTest/ViewController.swift b/SampleApps/CarthageTest/CarthageTest/ViewController.swift index 9e369e5dc4..88570aae90 100644 --- a/SampleApps/CarthageTest/CarthageTest/ViewController.swift +++ b/SampleApps/CarthageTest/CarthageTest/ViewController.swift @@ -16,7 +16,7 @@ class ViewController: UIViewController { override func viewDidLoad() { let apiClient = BTAPIClient(authorization: "sandbox_9dbg82cq_dcpspy2brwdjr3qn")! - let amexClient = BTAmericanExpressClient(apiClient: apiClient) + let amexClient = BTAmericanExpressClient(authorization: "sandbox_9dbg82cq_dcpspy2brwdjr3qn") let applePayClient = BTApplePayClient(apiClient: apiClient) let cardClient = BTCardClient(apiClient: apiClient) let dataCollector = BTDataCollector(apiClient: apiClient) diff --git a/SampleApps/SPMTest/SPMTest/ViewController.swift b/SampleApps/SPMTest/SPMTest/ViewController.swift index b1525c84a1..b6a3941bcd 100644 --- a/SampleApps/SPMTest/SPMTest/ViewController.swift +++ b/SampleApps/SPMTest/SPMTest/ViewController.swift @@ -16,7 +16,7 @@ class ViewController: UIViewController { override func viewDidLoad() { let apiClient = BTAPIClient(authorization: "sandbox_9dbg82cq_dcpspy2brwdjr3qn")! - let amexClient = BTAmericanExpressClient(apiClient: apiClient) + let amexClient = BTAmericanExpressClient(authorization: "sandbox_9dbg82cq_dcpspy2brwdjr3qn") let applePayClient = BTApplePayClient(apiClient: apiClient) let cardClient = BTCardClient(apiClient: apiClient) let dataCollector = BTDataCollector(apiClient: apiClient) From 0ae85a7395d573df4a2da1932996fff6d14924f3 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 21 Feb 2025 08:34:55 -0600 Subject: [PATCH 10/14] PR feedback: update URL to paypal.com --- .../BraintreeCore/Authorization/InvalidAuthorization.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift index b47a902ad2..e611f55e3d 100644 --- a/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift +++ b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift @@ -13,6 +13,8 @@ class InvalidAuthorization: ClientAuthorization { self.originalValue = rawValue // swiftlint:disable:next force_unwrapping - self.configURL = URL(string: "https://example.com")! + /// This URL is never used in the SDK as we always return an error if the authorization type is `.invalidAuthorization` + /// before construting or using the `configURL` in any way + self.configURL = URL(string: "https://paypal.com")! } } From e6f01c3d0758b4e2ed5d1a58a24dc1a79689b97d Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 21 Feb 2025 08:38:34 -0600 Subject: [PATCH 11/14] update for linting --- .../BraintreeCore/Authorization/InvalidAuthorization.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift index e611f55e3d..7766868f3d 100644 --- a/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift +++ b/Sources/BraintreeCore/Authorization/InvalidAuthorization.swift @@ -11,8 +11,8 @@ class InvalidAuthorization: ClientAuthorization { init(_ rawValue: String) { self.bearer = rawValue self.originalValue = rawValue - - // swiftlint:disable:next force_unwrapping + + // swiftlint:disable force_unwrapping /// This URL is never used in the SDK as we always return an error if the authorization type is `.invalidAuthorization` /// before construting or using the `configURL` in any way self.configURL = URL(string: "https://paypal.com")! From 703725c3a13be0ba7cfaf13532ddf15adb25e6af Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 21 Feb 2025 15:44:51 -0600 Subject: [PATCH 12/14] Update V7_MIGRATION.md Co-authored-by: scannillo <35243507+scannillo@users.noreply.github.com> --- V7_MIGRATION.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/V7_MIGRATION.md b/V7_MIGRATION.md index 3ae44166d3..fd421087ed 100644 --- a/V7_MIGRATION.md +++ b/V7_MIGRATION.md @@ -74,5 +74,4 @@ use the [PayPal (web)](https://developer.paypal.com/braintree/docs/guides/paypal Update initializer for `BTAmericanExpressClient`: ```diff - var amexClient = BTAmericanExpressClient(apiClient: apiClient) -+ var amexClient = BTAmericanExpressClient(authorization: authorization) -``` ++ var amexClient = BTAmericanExpressClient(authorization: "") From 15bed089b3ea90137feb8ed9e4c3bbb8834ff73c Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 21 Feb 2025 16:16:15 -0600 Subject: [PATCH 13/14] PR feedbak: move authorization.type check to requests --- Sources/BraintreeCore/BTAPIClient.swift | 40 ++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index dc440cda3b..442eb7eee3 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -121,28 +121,19 @@ 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? - - if authorization.type == .invalidAuthorization { - completion(nil, BTAPIClientError.invalidAuthorization(authorization.originalValue)) - } else { - Task { @MainActor in - do { - let configuration = try await configurationLoader.getConfig() - setupHTTPCredentials(configuration) - completion(configuration, nil) - } catch { - completion(nil, error) - } + Task { @MainActor in + do { + let configuration = try await configurationLoader.getConfig() + setupHTTPCredentials(configuration) + completion(configuration, nil) + } catch { + completion(nil, error) } } } @MainActor func fetchConfiguration() async throws -> BTConfiguration { - if authorization.type == .invalidAuthorization { - throw BTAPIClientError.invalidAuthorization(authorization.originalValue) - } else { - try await configurationLoader.getConfig() - } + try await configurationLoader.getConfig() } /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. @@ -163,6 +154,11 @@ import Foundation httpType: BTAPIClientHTTPService = .gateway, completion: @escaping RequestCompletion ) { + if authorization.type == .invalidAuthorization { + completion(nil, nil, BTAPIClientError.invalidAuthorization(authorization.originalValue)) + return + } + fetchOrReturnRemoteConfiguration { [weak self] configuration, error in guard let self else { completion(nil, nil, BTAPIClientError.deallocated) @@ -198,6 +194,11 @@ import Foundation httpType: BTAPIClientHTTPService = .gateway, completion: @escaping RequestCompletion ) { + if authorization.type == .invalidAuthorization { + completion(nil, nil, BTAPIClientError.invalidAuthorization(authorization.originalValue)) + return + } + fetchOrReturnRemoteConfiguration { [weak self] configuration, error in guard let self else { completion(nil, nil, BTAPIClientError.deallocated) @@ -233,6 +234,11 @@ import Foundation httpType: BTAPIClientHTTPService = .gateway, completion: @escaping RequestCompletion ) { + if authorization.type == .invalidAuthorization { + completion(nil, nil, BTAPIClientError.invalidAuthorization(authorization.originalValue)) + return + } + fetchOrReturnRemoteConfiguration { [weak self] configuration, error in guard let self else { completion(nil, nil, BTAPIClientError.deallocated) From c627783b66e6d5948ca7bd7ec36e03b5794dd590 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Mon, 24 Feb 2025 12:07:14 -0600 Subject: [PATCH 14/14] Apply suggestions from code review Co-authored-by: Rich Herrera --- Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift index fe74e8e9d9..6fc27b5ac7 100644 --- a/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift +++ b/Sources/BraintreeAmericanExpress/BTAmericanExpressClient.swift @@ -11,7 +11,7 @@ import BraintreeCore var apiClient: BTAPIClient /// Creates an American Express client. - /// - Parameter authorization: Your client token or tokenization key + /// - Parameter authorization: A valid client token or tokenization key used to authorize API calls @objc(initWithAPIClient:) public init(authorization: String) { self.apiClient = BTAPIClient(newAuthorization: authorization)