From ccfda37a03a7830386abbbb7cae2ddfd93a82973 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 12 Jul 2024 09:51:31 -0500 Subject: [PATCH 1/8] add swiftlint.yml file --- .github/workflows/swiftlint.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/swiftlint.yml diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml new file mode 100644 index 0000000000..81504daa1e --- /dev/null +++ b/.github/workflows/swiftlint.yml @@ -0,0 +1,18 @@ +name: Lint +on: [pull_request] +concurrency: + group: lint-${{ github.event.number }} + cancel-in-progress: true +jobs: + swiftlint: + name: SwiftLint + runs-on: macOS-14-xlarge + steps: + - name: Check out repository + uses: actions/checkout@v3 + - name: Use Xcode 15.0.1 + run: sudo xcode-select -switch /Applications/Xcode_15.0.1.app + - name: Install SwiftLint + run: brew install swiftlint + - name: Run SwiftLint + run: swiftlint --strict From a70aa0d77eb4a76fe14d34e0b90f2245c13d25d4 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 12 Jul 2024 12:11:14 -0500 Subject: [PATCH 2/8] add .swiftlint.yml and build script --- .swiftlint.yml | 106 ++++++++++++++++++++++++++++ Braintree.xcodeproj/project.pbxproj | 21 ++++++ 2 files changed, 127 insertions(+) create mode 100644 .swiftlint.yml diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000000..66042ee3d8 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,106 @@ +# Reference: https://github.com/realm/SwiftLint +# Required Swiftlint Version +# swiftlint_version: 0.39.2 + +# Paths to include in lint +included: + - Sources/BraintreeCore + +excluded: + - Sources/BraintreeCore/BTAPIPinnedCertificates.swift + +disabled_rules: + - todo + - type_name # tests will have have the format _Tests + - xctfail_message + - blanket_disable_command + - non_optional_string_data_conversion + - attributes + - multiline_function_chains + +opt_in_rules: + - array_init + - closure_end_indentation + - closure_spacing + - collection_alignment + - colon # promote to error + - convenience_type + - discouraged_object_literal + - empty_collection_literal + - empty_count + - empty_string + - enum_case_associated_values_count + - fatal_error_message + - first_where + - force_unwrapping + - implicitly_unwrapped_optional + - indentation_width + - last_where + - legacy_random + - literal_expression_end_indentation + - multiline_arguments + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - operator_usage_whitespace + - overridden_super_call + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - redundant_nil_coalescing + - redundant_type_annotation + - strict_fileprivate + - toggle_bool + - trailing_closure + - unneeded_parentheses_in_closure_argument + - vertical_whitespace_closing_braces + - yoda_condition + +custom_rules: + array_constructor: + name: "Array/Dictionary initializer" + regex: '[let,var] .+ = (\[.+\]\(\))' + capture_group: 1 + message: "Use explicit type annotation when initializing empty arrays and dictionaries" + severity: warning + space_after_main_type: + name: "No space after main type" + regex: '(class|struct|extension)((?-s)\s.*\{$\n)(?!^\s*$)' + message: "Empty line required after main declarations" + severity: warning + +force_cast: warning +force_try: warning +function_body_length: + warning: 60 + +legacy_hashing: error + +identifier_name: + excluded: + - i + - id + - x + - y + - z + +indentation_width: + indentation_width: 4 + +line_length: + warning: 140 + ignores_urls: true + ignores_comments: true + +multiline_arguments: + first_argument_location: next_line + only_enforce_after_first_closure_on_first_line: true + +private_over_fileprivate: + validate_extensions: true + +trailing_whitespace: + ignores_empty_lines: true + +vertical_whitespace: + max_empty_lines: 2 diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 28541553bd..7ea3a7cca2 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -2237,6 +2237,7 @@ 570B9385285397520041BAFE /* Frameworks */, 570B9386285397520041BAFE /* Headers */, 570B93A8285397520041BAFE /* Resources */, + BE676C532C417B8F000A6579 /* Swiftlint */, ); buildRules = ( ); @@ -3154,6 +3155,26 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + BE676C532C417B8F000A6579 /* Swiftlint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = Swiftlint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/sh\n\nif test -d \"/opt/homebrew/bin/\"; then\n PATH=\"/opt/homebrew/bin/:${PATH}\"\nfi\n\nexport PATH\n\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + showEnvVarsInLog = 0; + }; CDAB67F3BC5BE564FCFFD6BC /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; From 91c32273ac7992c1c3f1229ff554af1d540fadc4 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 12 Jul 2024 12:11:44 -0500 Subject: [PATCH 3/8] address swiftlint warnings for BraintreeCore --- .../Analytics/BTAnalyticsService.swift | 8 +- .../Analytics/FPTIBatchData.swift | 11 ++- .../Authorization/BTClientToken.swift | 11 ++- .../Authorization/BTClientTokenError.swift | 2 + .../Authorization/TokenizationKey.swift | 6 +- .../Authorization/TokenizationKeyError.swift | 1 - Sources/BraintreeCore/BTAPIClient.swift | 26 ++++-- .../BraintreeCore/BTAppContextSwitcher.swift | 12 ++- Sources/BraintreeCore/BTCoreConstants.swift | 2 + .../BraintreeCore/BTGraphQLErrorTree.swift | 2 +- Sources/BraintreeCore/BTGraphQLHTTP.swift | 19 ++++- .../BTGraphQLMultiErrorNode.swift | 2 +- .../BTGraphQLSingleErrorNode.swift | 2 +- Sources/BraintreeCore/BTHTTP.swift | 83 +++++++++++++------ Sources/BraintreeCore/BTJSON.swift | 7 +- Sources/BraintreeCore/BTJSONError.swift | 4 +- Sources/BraintreeCore/BTLogLevel.swift | 4 +- .../BTPaymentMethodNonceParser.swift | 2 + Sources/BraintreeCore/BTPostalAddress.swift | 15 ++-- Sources/BraintreeCore/BTURLUtils.swift | 11 ++- .../BTWebAuthenticationSession.swift | 10 +-- .../BraintreeCore/ConfigurationLoader.swift | 2 +- .../UIApplication+URLOpener.swift | 2 +- 23 files changed, 158 insertions(+), 86 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index f92c0e3e61..b73916eb98 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -4,8 +4,10 @@ class BTAnalyticsService: Equatable { // MARK: - Internal Properties + // swiftlint:disable force_unwrapping /// The FPTI URL to post all analytic events. static let url = URL(string: "https://api.paypal.com")! + // swiftlint:enable force_unwrapping /// The HTTP client for communication with the analytics service endpoint. Exposed for testing. var http: BTHTTP? @@ -131,7 +133,11 @@ class BTAnalyticsService: Equatable { if await !BTAnalyticsService.events.isEmpty { do { let configuration = try await apiClient.fetchConfiguration() - let postParameters = await createAnalyticsEvent(config: configuration, sessionID: apiClient.metadata.sessionID, events: Self.events.allValues) + let postParameters = await createAnalyticsEvent( + config: configuration, + sessionID: apiClient.metadata.sessionID, + events: Self.events.allValues + ) http?.post("v1/tracking/batch/events", parameters: postParameters) { _, _, _ in } await Self.events.removeAll() } catch { diff --git a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift index f79f471f0b..0083e3380c 100644 --- a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift +++ b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift @@ -1,15 +1,18 @@ import UIKit +// swiftlint:disable nesting /// The POST body for a batch upload of FPTI events struct FPTIBatchData: Codable { let events: [EventsContainer] // Single-element "events" array required by FPTI formatting init(metadata: Metadata, events fptiEvents: [Event]?) { - self.events = [EventsContainer( - metadata: metadata, - fptiEvents: fptiEvents ?? [] - )] + self.events = [ + EventsContainer( + metadata: metadata, + fptiEvents: fptiEvents ?? [] + ) + ] } struct EventsContainer: Codable { diff --git a/Sources/BraintreeCore/Authorization/BTClientToken.swift b/Sources/BraintreeCore/Authorization/BTClientToken.swift index c88a0317ba..2621118919 100644 --- a/Sources/BraintreeCore/Authorization/BTClientToken.swift +++ b/Sources/BraintreeCore/Authorization/BTClientToken.swift @@ -2,8 +2,9 @@ import Foundation /// An authorization string used to initialize the Braintree SDK @_documentation(visibility: private) -@objcMembers public class BTClientToken: NSObject, NSCoding, NSCopying, ClientAuthorization { - +@objcMembers +public class BTClientToken: NSObject, NSCoding, NSCopying, ClientAuthorization { + // NEXT_MAJOR_VERSION (v7): properties exposed for Objective-C interoperability + Drop-in access. // Once the entire SDK is in Swift, determine if we want public properties to be internal and // what we can make internal without breaking the Drop-in. @@ -38,8 +39,7 @@ import Foundation // Client token must be decoded first because the other values are retrieved from it self.json = try Self.decodeClientToken(clientToken) - guard let authorizationFingerprint = json["authorizationFingerprint"].asString(), - !authorizationFingerprint.isEmpty else { + guard let authorizationFingerprint = json["authorizationFingerprint"].asString(), !authorizationFingerprint.isEmpty else { throw BTClientTokenError.invalidAuthorizationFingerprint } @@ -113,8 +113,7 @@ import Foundation // MARK: - NSObject override public override func isEqual(_ object: Any?) -> Bool { - guard object is BTClientToken, - let otherToken = object as? BTClientToken else { + guard object is BTClientToken, let otherToken = object as? BTClientToken else { return false } diff --git a/Sources/BraintreeCore/Authorization/BTClientTokenError.swift b/Sources/BraintreeCore/Authorization/BTClientTokenError.swift index 4920c76f2d..3e73ffbf1d 100644 --- a/Sources/BraintreeCore/Authorization/BTClientTokenError.swift +++ b/Sources/BraintreeCore/Authorization/BTClientTokenError.swift @@ -37,6 +37,7 @@ public enum BTClientTokenError: Error, CustomNSError, LocalizedError, Equatable } } + // swiftlint:disable line_length public var errorDescription: String? { switch self { case .invalidAuthorizationFingerprint: @@ -51,4 +52,5 @@ public enum BTClientTokenError: Error, CustomNSError, LocalizedError, Equatable return "Failed to decode client token. \(description)" } } + // swiftlint:enable line_length } diff --git a/Sources/BraintreeCore/Authorization/TokenizationKey.swift b/Sources/BraintreeCore/Authorization/TokenizationKey.swift index 5cf6d03034..e291045b47 100644 --- a/Sources/BraintreeCore/Authorization/TokenizationKey.swift +++ b/Sources/BraintreeCore/Authorization/TokenizationKey.swift @@ -27,10 +27,10 @@ class TokenizationKey: ClientAuthorization { guard tokenizationKey.range(of: pattern, options: .regularExpression) != nil else { return nil } let tokenizationKeyParts = tokenizationKey.split(separator: "_", maxSplits: 3) - let environment: String = String(tokenizationKeyParts[0]) - let merchantID: String = String(tokenizationKeyParts[2]) + let environment = String(tokenizationKeyParts[0]) + let merchantID = String(tokenizationKeyParts[2]) - var components: URLComponents = URLComponents() + var components = URLComponents() components.scheme = environment == "development" ? "http" : "https" guard let host = host(for: environment) else { return nil } diff --git a/Sources/BraintreeCore/Authorization/TokenizationKeyError.swift b/Sources/BraintreeCore/Authorization/TokenizationKeyError.swift index f24780889e..44d9c35e19 100644 --- a/Sources/BraintreeCore/Authorization/TokenizationKeyError.swift +++ b/Sources/BraintreeCore/Authorization/TokenizationKeyError.swift @@ -21,4 +21,3 @@ public enum TokenizationKeyError: Int, Error, CustomNSError, LocalizedError, Equ } } } - diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index e567812921..be507bec08 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -1,5 +1,6 @@ import Foundation +// 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 { @@ -46,7 +47,7 @@ import Foundation switch authorizationType { case .tokenizationKey: do { - self.authorization = try TokenizationKey(authorization) + self.authorization = try TokenizationKey(authorization) } catch { return nil } @@ -68,7 +69,7 @@ import Foundation http?.networkTimingDelegate = self // Kickoff the background request to fetch the config - fetchOrReturnRemoteConfiguration { configuration, error in + fetchOrReturnRemoteConfiguration { _, _ in // No-op } } @@ -148,7 +149,7 @@ import Foundation "session_id": metadata.sessionID ] - get("v1/payment_methods", parameters: parameters) { body, response, error in + get("v1/payment_methods", parameters: parameters) { body, _, error in if let error { completion(nil, error) return @@ -158,7 +159,10 @@ import Foundation body?["paymentMethods"].asArray()?.forEach { paymentInfo in let type: String? = paymentInfo["type"].asString() - let paymentMethodNonce: BTPaymentMethodNonce? = BTPaymentMethodNonceParser.shared.parseJSON(paymentInfo, withParsingBlockForType: type) + let paymentMethodNonce: BTPaymentMethodNonce? = BTPaymentMethodNonceParser.shared.parseJSON( + paymentInfo, + withParsingBlockForType: type + ) if let paymentMethodNonce { paymentMethodNonces.append(paymentMethodNonce) @@ -269,7 +273,13 @@ import Foundation } let postParameters = BTAPIRequest(requestBody: parameters, metadata: metadata, httpType: httpType) - http(for: httpType)?.post(path, configuration: configuration, parameters: postParameters, headers: headers, completion: completion) + http(for: httpType)?.post( + path, + configuration: configuration, + parameters: postParameters, + headers: headers, + completion: completion + ) } } @@ -339,7 +349,11 @@ import Foundation 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 tokenizationKeyMatch: NSTextCheckingResult? = regularExpression.firstMatch(in: authorization, options: [], range: NSRange(location: 0, length: authorization.count)) + let tokenizationKeyMatch: NSTextCheckingResult? = regularExpression.firstMatch( + in: authorization, + options: [], + range: NSRange(location: 0, length: authorization.count) + ) return tokenizationKeyMatch != nil ? .tokenizationKey : .clientToken } diff --git a/Sources/BraintreeCore/BTAppContextSwitcher.swift b/Sources/BraintreeCore/BTAppContextSwitcher.swift index 913665c4b4..db875e268d 100644 --- a/Sources/BraintreeCore/BTAppContextSwitcher.swift +++ b/Sources/BraintreeCore/BTAppContextSwitcher.swift @@ -18,8 +18,8 @@ import UIKit // MARK: - Private Properties - private var appContextSwitchClients = [BTAppContextSwitchClient.Type]() - + private var appContextSwitchClients: [BTAppContextSwitchClient.Type] = [] + // MARK: - Public Methods /// Determine whether the return URL can be handled. @@ -35,11 +35,9 @@ import UIKit /// - Returns: `true` when the SDK has handled the URL successfully @objc(handleOpenURL:) @discardableResult public func handleOpen(_ url: URL) -> Bool { - for appContextSwitchClient in appContextSwitchClients { - if appContextSwitchClient.canHandleReturnURL(url) { - appContextSwitchClient.handleReturnURL(url) - return true - } + for appContextSwitchClient in appContextSwitchClients where appContextSwitchClient.canHandleReturnURL(url) { + appContextSwitchClient.handleReturnURL(url) + return true } return false } diff --git a/Sources/BraintreeCore/BTCoreConstants.swift b/Sources/BraintreeCore/BTCoreConstants.swift index 2af6082577..7727845b96 100644 --- a/Sources/BraintreeCore/BTCoreConstants.swift +++ b/Sources/BraintreeCore/BTCoreConstants.swift @@ -20,9 +20,11 @@ import Foundation static let graphQLVersion: String = "2018-03-06" + // swiftlint:disable force_unwrapping static let payPalProductionURL = URL(string: "https://api.paypal.com")! static let payPalSandboxURL = URL(string: "https://api.sandbox.paypal.com")! + // swiftlint:enable force_unwrapping // MARK: - BTHTTPError Constants diff --git a/Sources/BraintreeCore/BTGraphQLErrorTree.swift b/Sources/BraintreeCore/BTGraphQLErrorTree.swift index f31d8dc6bf..fa290fb4c1 100644 --- a/Sources/BraintreeCore/BTGraphQLErrorTree.swift +++ b/Sources/BraintreeCore/BTGraphQLErrorTree.swift @@ -3,7 +3,7 @@ import Foundation class BTGraphQLErrorTree { let message: String - let rootNode: BTGraphQLMultiErrorNode = BTGraphQLMultiErrorNode() + let rootNode = BTGraphQLMultiErrorNode() init(message: String) { self.message = message diff --git a/Sources/BraintreeCore/BTGraphQLHTTP.swift b/Sources/BraintreeCore/BTGraphQLHTTP.swift index 7d74b6bdf1..1285efa5ee 100644 --- a/Sources/BraintreeCore/BTGraphQLHTTP.swift +++ b/Sources/BraintreeCore/BTGraphQLHTTP.swift @@ -6,15 +6,26 @@ class BTGraphQLHTTP: BTHTTP { // MARK: - Properties - private let exceptionName: NSExceptionName = NSExceptionName("") + private let exceptionName = NSExceptionName("") // MARK: - Overrides - override func get(_ path: String, configuration: BTConfiguration? = nil, parameters: Encodable? = nil, completion: @escaping RequestCompletion) { + override func get( + _ path: String, + configuration: BTConfiguration? = nil, + parameters: Encodable? = nil, + completion: @escaping RequestCompletion + ) { NSException(name: exceptionName, reason: "GET is unsupported").raise() } - override func post(_ path: String, configuration: BTConfiguration? = nil, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping RequestCompletion) { + override func post( + _ path: String, + configuration: BTConfiguration? = nil, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping RequestCompletion + ) { httpRequest(method: "POST", configuration: configuration, parameters: parameters, completion: completion) } @@ -103,7 +114,7 @@ class BTGraphQLHTTP: BTHTTP { let body = BTJSON(value: json) // Success case - if let _ = body.asDictionary(), body["errors"].asArray() == nil { + if body.asDictionary() != nil, body["errors"].asArray() == nil { callCompletionAsync(with: completion, body: body, response: httpResponse, error: nil) return } diff --git a/Sources/BraintreeCore/BTGraphQLMultiErrorNode.swift b/Sources/BraintreeCore/BTGraphQLMultiErrorNode.swift index 645439231a..4e7d0ea11b 100644 --- a/Sources/BraintreeCore/BTGraphQLMultiErrorNode.swift +++ b/Sources/BraintreeCore/BTGraphQLMultiErrorNode.swift @@ -43,7 +43,7 @@ class BTGraphQLMultiErrorNode: BTGraphQLErrorNode { return result } - func toDictionary() -> [String : Any] { + func toDictionary() -> [String: Any] { var result: [String: Any] = ["field": field] result["fieldErrors"] = mapChildrenInOrder { $0.toDictionary() } return result diff --git a/Sources/BraintreeCore/BTGraphQLSingleErrorNode.swift b/Sources/BraintreeCore/BTGraphQLSingleErrorNode.swift index ef49e9d2df..00e054e93f 100644 --- a/Sources/BraintreeCore/BTGraphQLSingleErrorNode.swift +++ b/Sources/BraintreeCore/BTGraphQLSingleErrorNode.swift @@ -12,7 +12,7 @@ class BTGraphQLSingleErrorNode: BTGraphQLErrorNode { self.code = code } - func toDictionary() -> [String : Any] { + func toDictionary() -> [String: Any] { var result = ["field": field, "message": message] if let code = code { result["code"] = code diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index 1cc046507e..e67b33e256 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -1,6 +1,7 @@ import Foundation import Security +// swiftlint:disable type_body_length file_length /// Performs HTTP methods on the Braintree Client API class BTHTTP: NSObject, URLSessionTaskDelegate { @@ -12,7 +13,7 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { let pinnedCertificates: [Data] = BTAPIPinnedCertificates.trustedCertificates() /// DispatchQueue on which asynchronous code will be executed. Defaults to `DispatchQueue.main`. - var dispatchQueue: DispatchQueue = DispatchQueue.main + var dispatchQueue = DispatchQueue.main /// A URL set to override the URLs derived from the ClientAuthorization or BTConfiguration response let customBaseURL: URL? @@ -23,10 +24,10 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { /// Session exposed for testing lazy var session: URLSession = { - let configuration: URLSessionConfiguration = URLSessionConfiguration.ephemeral + let configuration = URLSessionConfiguration.ephemeral configuration.httpAdditionalHeaders = defaultHeaders - let delegateQueue: OperationQueue = OperationQueue() + let delegateQueue = OperationQueue() delegateQueue.name = "com.braintreepayments.BTHTTP" return URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue) @@ -76,11 +77,30 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { } // TODO: - Remove when all POST bodies use Codable, instead of BTJSON/raw dictionaries - func post(_ path: String, configuration: BTConfiguration? = nil, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping RequestCompletion) { - httpRequest(method: "POST", path: path, configuration: configuration, parameters: parameters, headers: headers, completion: completion) + func post( + _ path: String, + configuration: BTConfiguration? = nil, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping RequestCompletion + ) { + httpRequest( + method: "POST", + path: path, + configuration: configuration, + parameters: parameters, + headers: headers, + completion: completion + ) } - func post(_ path: String, configuration: BTConfiguration? = nil, parameters: Encodable, headers: [String: String]? = nil, completion: @escaping RequestCompletion) { + func post( + _ path: String, + configuration: BTConfiguration? = nil, + parameters: Encodable, + headers: [String: String]? = nil, + completion: @escaping RequestCompletion + ) { do { let dict = try parameters.toDictionary() post(path, configuration: configuration, parameters: dict, headers: headers, completion: completion) @@ -100,8 +120,14 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { completion: RequestCompletion? ) { do { - let request = try createRequest(method: method, path: path, configuration: configuration, parameters: parameters, headers: headers) - + let request = try createRequest( + method: method, + path: path, + configuration: configuration, + parameters: parameters, + headers: headers + ) + self.session.dataTask(with: request) { [weak self] data, response, error in guard let self else { completion?(nil, nil, BTHTTPError.deallocated("BTHTTP")) @@ -139,7 +165,7 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { throw BTHTTPError.missingBaseURL(errorUserInfo) } - let mutableParameters: NSMutableDictionary = NSMutableDictionary(dictionary: parameters ?? [:]) + let mutableParameters = NSMutableDictionary(dictionary: parameters ?? [:]) // TODO: - Investigate for parity on JS and Android // JIRA - DTBTSDK-2682 @@ -161,7 +187,7 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { parameters: NSMutableDictionary? = [:], headers additionalHeaders: [String: String]? = nil ) throws -> URLRequest { - guard var components: URLComponents = URLComponents(string: url.absoluteString) else { + guard var components = URLComponents(string: url.absoluteString) else { throw BTHTTPError.urlStringInvalid } @@ -235,13 +261,12 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { return } - guard let response = response, - let httpResponse = createHTTPResponse(response: response) else { + guard let response, let httpResponse = createHTTPResponse(response: response) else { callCompletionAsync(with: completion, body: nil, response: nil, error: BTHTTPError.httpResponseInvalid) return } - guard let data = data else { + guard let data else { callCompletionAsync(with: completion, body: nil, response: nil, error: BTHTTPError.dataNotFound) return } @@ -301,7 +326,7 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { errorUserInfo[NSLocalizedFailureReasonErrorKey] = [HTTPURLResponse.localizedString(forStatusCode: response.statusCode)] - var json: BTJSON = BTJSON() + var json = BTJSON() if responseContentType == "application/json" { json = data.isEmpty ? BTJSON() : BTJSON(data: data) @@ -315,7 +340,7 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { } } - var error: BTHTTPError = BTHTTPError.clientError(errorUserInfo) + var error = BTHTTPError.clientError(errorUserInfo) if response.statusCode == 429 { errorUserInfo[NSLocalizedDescriptionKey] = "You are being rate-limited." @@ -337,11 +362,12 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { completion: @escaping (Error?) -> Void ) { let responseContentType: String? = response.mimeType - var errorUserInfo: [String : Any] = [BTCoreConstants.urlResponseKey: response] + var errorUserInfo: [String: Any] = [BTCoreConstants.urlResponseKey: response] if let contentType = responseContentType, contentType != "application/json" { // Return error for unsupported response type - errorUserInfo[NSLocalizedFailureReasonErrorKey] = "BTHTTP only supports application/json responses, received Content-Type: \(contentType)" + let message = "BTHTTP only supports application/json responses, received Content-Type: \(contentType)" + errorUserInfo[NSLocalizedFailureReasonErrorKey] = message completion(BTHTTPError.responseContentTypeNotAcceptable(errorUserInfo)) } else { completion(json.asError()) @@ -363,10 +389,16 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { // MARK: - URLSessionTaskDelegate conformance - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { let domain: String = challenge.protectionSpace.host + // swiftlint:disable force_unwrapping let serverTrust: SecTrust = challenge.protectionSpace.serverTrust! + // swiftlint:enable force_unwrapping let policies: [SecPolicy] = [SecPolicyCreateSSL(true, domain as CFString)] SecTrustSetPolicies(serverTrust, policies as CFArray) @@ -376,7 +408,7 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { let trusted: Bool = SecTrustEvaluateWithError(serverTrust, &error) if trusted && error == nil { - let credential: URLCredential = URLCredential(trust: serverTrust) + let credential = URLCredential(trust: serverTrust) completionHandler(.useCredential, credential) } else { completionHandler(.rejectProtectionSpace, nil) @@ -389,13 +421,12 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { metrics.transactionMetrics.forEach { transaction in if let startDate = transaction.fetchStartDate, - let endDate = transaction.responseEndDate, - var path = transaction.request.url?.path { - - if path.contains("graphql"), - let data = task.originalRequest?.httpBody, - let mutationName = getGraphQLMutationName(data) { - path = mutationName + let endDate = transaction.responseEndDate, + var path = transaction.request.url?.path { + if path.contains("graphql"), + let data = task.originalRequest?.httpBody, + let mutationName = getGraphQLMutationName(data) { + path = mutationName } networkTimingDelegate?.fetchAPITiming( diff --git a/Sources/BraintreeCore/BTJSON.swift b/Sources/BraintreeCore/BTJSON.swift index 5f7abbef48..47b67b29fd 100644 --- a/Sources/BraintreeCore/BTJSON.swift +++ b/Sources/BraintreeCore/BTJSON.swift @@ -42,6 +42,7 @@ import Foundation /// ``` @_documentation(visibility: private) @objcMembers public class BTJSON: NSObject { + var value: Any? = [:] as [AnyHashable?: Any] // MARK: Initializers @@ -157,8 +158,7 @@ import Foundation return self } - guard let value = value as? [String: Any], - let unwrappedResult = value[key] else { + guard let value = value as? [String: Any], let unwrappedResult = value[key] else { return BTJSON(value: BTJSONError.keyInvalid(key)) } return BTJSON(value: unwrappedResult) @@ -254,8 +254,7 @@ import Foundation /// - orDefault: The default value if conversion fails /// - Returns: An `Enum` representing the `BTJSON` instance public func asEnum(_ mapping: [String: Any], orDefault: Int) -> Int { - guard let key = value as? String, - let result: Int = mapping[key] as? Int else { + guard let key = value as? String, let result: Int = mapping[key] as? Int else { return orDefault } diff --git a/Sources/BraintreeCore/BTJSONError.swift b/Sources/BraintreeCore/BTJSONError.swift index 82b880ccc3..c0d9c795c8 100644 --- a/Sources/BraintreeCore/BTJSONError.swift +++ b/Sources/BraintreeCore/BTJSONError.swift @@ -19,9 +19,9 @@ public enum BTJSONError: Error, CustomNSError, LocalizedError, Equatable { switch self { case .jsonSerializationFailure: return 0 - case .indexInvalid(_): + case .indexInvalid: return 1 - case .keyInvalid(_): + case .keyInvalid: return 2 } } diff --git a/Sources/BraintreeCore/BTLogLevel.swift b/Sources/BraintreeCore/BTLogLevel.swift index af267d6916..828c91e65a 100644 --- a/Sources/BraintreeCore/BTLogLevel.swift +++ b/Sources/BraintreeCore/BTLogLevel.swift @@ -1,9 +1,9 @@ import Foundation -/// :nodoc: This enum 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. -/// Log level used to add formatted string to NSLog // TODO: when all modules are converted to Swift, we should used a var on this enum for the description vs using a separate class as a wrapper for Obj-C compatibility // TODO: use Foundations Logger instead of NSLog once all modules are in Swift +/// :nodoc: This enum 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. +/// Log level used to add formatted string to NSLog @_documentation(visibility: private) public enum BTLogLevel: Int { diff --git a/Sources/BraintreeCore/BTPaymentMethodNonceParser.swift b/Sources/BraintreeCore/BTPaymentMethodNonceParser.swift index 7268e8bd44..1af8153098 100644 --- a/Sources/BraintreeCore/BTPaymentMethodNonceParser.swift +++ b/Sources/BraintreeCore/BTPaymentMethodNonceParser.swift @@ -96,6 +96,7 @@ import Foundation } } + // swiftlint:disable cyclomatic_complexity private func cardType(from cardType: String) -> String { let cardType = cardType.lowercased() @@ -129,4 +130,5 @@ import Foundation return "Unknown" } + // swiftlint:enable cyclomatic_complexity } diff --git a/Sources/BraintreeCore/BTPostalAddress.swift b/Sources/BraintreeCore/BTPostalAddress.swift index aae0912e0d..30a9ba6179 100644 --- a/Sources/BraintreeCore/BTPostalAddress.swift +++ b/Sources/BraintreeCore/BTPostalAddress.swift @@ -2,28 +2,29 @@ import Foundation /// Generic postal address @objcMembers public class BTPostalAddress: NSObject { + // Property names follow the `Braintree_Address` convention as documented at: // https://developer.paypal.com/braintree/docs/reference/request/address/create /// Optional. Recipient name for shipping address. - public var recipientName: String? = nil + public var recipientName: String? /// Line 1 of the Address (eg. number, street, etc). - public var streetAddress: String? = nil + public var streetAddress: String? /// Optional line 2 of the Address (eg. suite, apt #, etc.). - public var extendedAddress: String? = nil + public var extendedAddress: String? /// City name - public var locality: String? = nil + public var locality: String? /// 2 letter country code. - public var countryCodeAlpha2: String? = nil + public var countryCodeAlpha2: String? /// Zip code or equivalent is usually required for countries that have them. /// For a list of countries that do not have postal codes please refer to http://en.wikipedia.org/wiki/Postal_code. - public var postalCode: String? = nil + public var postalCode: String? /// Either a two-letter state code (for the US), or an ISO-3166-2 country subdivision code of up to three letters. - public var region: String? = nil + public var region: String? } diff --git a/Sources/BraintreeCore/BTURLUtils.swift b/Sources/BraintreeCore/BTURLUtils.swift index c7605b4700..a7d29ff4ca 100644 --- a/Sources/BraintreeCore/BTURLUtils.swift +++ b/Sources/BraintreeCore/BTURLUtils.swift @@ -1,7 +1,7 @@ import Foundation /// :nodoc: This class 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. -///A helper class for converting URL queries to and from dictionaries +/// A helper class for converting URL queries to and from dictionaries @_documentation(visibility: private) @objc public class BTURLUtils: NSObject { @@ -25,9 +25,14 @@ import Foundation } } else if let dictValue = value as? [String: String] { for (subKey, subValue) in dictValue { - queryString = queryString.appendingFormat("%@%%5B%@%%5D=%@&", encodedKey, encode(subKey.description), encode(subValue.description)) + queryString = queryString.appendingFormat( + "%@%%5B%@%%5D=%@&", + encodedKey, + encode(subKey.description), + encode(subValue.description) + ) } - } else if let _ = value as? NSNull { + } else if value as? NSNull != nil { queryString = queryString.appendingFormat("%@=&", encodedKey) } else { queryString = queryString.appendingFormat("%@=%@&", encodedKey, encode(String(describing: value).description)) diff --git a/Sources/BraintreeCore/BTWebAuthenticationSession.swift b/Sources/BraintreeCore/BTWebAuthenticationSession.swift index fb6c54baa5..554d295a83 100644 --- a/Sources/BraintreeCore/BTWebAuthenticationSession.swift +++ b/Sources/BraintreeCore/BTWebAuthenticationSession.swift @@ -20,12 +20,12 @@ public class BTWebAuthenticationSession: NSObject { url: url, callbackURLScheme: BTCoreConstants.callbackURLScheme ) { url, error in - if let error = error as? NSError, error.code == ASWebAuthenticationSessionError.canceledLogin.rawValue { - sessionDidCancel() - } else { - sessionDidComplete(url, error) - } + if let error = error as? NSError, error.code == ASWebAuthenticationSessionError.canceledLogin.rawValue { + sessionDidCancel() + } else { + sessionDidComplete(url, error) } + } authenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession ?? false diff --git a/Sources/BraintreeCore/ConfigurationLoader.swift b/Sources/BraintreeCore/ConfigurationLoader.swift index d64636fd38..7583d4d1fb 100644 --- a/Sources/BraintreeCore/ConfigurationLoader.swift +++ b/Sources/BraintreeCore/ConfigurationLoader.swift @@ -5,7 +5,7 @@ class ConfigurationLoader { // MARK: - Private Properties private let configPath = "v1/configuration" - private let configurationCache: ConfigurationCache = ConfigurationCache.shared + private let configurationCache = ConfigurationCache.shared private let http: BTHTTP // MARK: - Intitializer diff --git a/Sources/BraintreeCore/UIApplication+URLOpener.swift b/Sources/BraintreeCore/UIApplication+URLOpener.swift index 4b68101986..7b7c75e8b6 100644 --- a/Sources/BraintreeCore/UIApplication+URLOpener.swift +++ b/Sources/BraintreeCore/UIApplication+URLOpener.swift @@ -34,9 +34,9 @@ extension UIApplication: URLOpener { return canOpenURL(payPalURL) } + // TODO: once Xcode 16 is the minimum supported version remove this method and update the protocol to the default open signature from UIApplication /// :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. /// Indicates whether the PayPal App is installed. - // TODO: once Xcode 16 is the minimum supported version remove this method and update the protocol to the default open signature from UIApplication @_documentation(visibility: private) public func open(_ url: URL, completionHandler completion: ((Bool) -> Void)?) { UIApplication.shared.open(url, options: [:], completionHandler: completion) From 03a0a9e96130b4bc144d7d38e69695d21a14e726 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 12 Jul 2024 12:25:53 -0500 Subject: [PATCH 4/8] cleanup --- Sources/BraintreeCore/Authorization/BTClientToken.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/BraintreeCore/Authorization/BTClientToken.swift b/Sources/BraintreeCore/Authorization/BTClientToken.swift index 2621118919..061ae0539c 100644 --- a/Sources/BraintreeCore/Authorization/BTClientToken.swift +++ b/Sources/BraintreeCore/Authorization/BTClientToken.swift @@ -2,8 +2,7 @@ import Foundation /// An authorization string used to initialize the Braintree SDK @_documentation(visibility: private) -@objcMembers -public class BTClientToken: NSObject, NSCoding, NSCopying, ClientAuthorization { +@objcMembers public class BTClientToken: NSObject, NSCoding, NSCopying, ClientAuthorization { // NEXT_MAJOR_VERSION (v7): properties exposed for Objective-C interoperability + Drop-in access. // Once the entire SDK is in Swift, determine if we want public properties to be internal and From a5c84c58c7d81b2e408c21a214f93f034715af48 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Mon, 15 Jul 2024 08:35:36 -0500 Subject: [PATCH 5/8] PR feedback: add installation instructions --- DEVELOPMENT.md | 10 ++++++++++ README.md | 3 +++ 2 files changed, 13 insertions(+) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e0b5b80967..210b00c758 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -6,6 +6,16 @@ This document outlines development practices that we follow while developing thi The included demo app utilizes a [sandbox sample merchant server](https://braintree-sample-merchant.herokuapp.com) hosted on Heroku. +## SwiftLint + +Ensure that you have [SwiftLint](https://github.com/realm/SwiftLint) installed as we utilize it within our project. + +To install via [Homebrew](https://brew.sh/) run: +``` +brew install swiftlint +``` +Our Xcode workspace has a `Run Phase` which integrates in `SwiftLint` so the only prerequisite is installing via `Homebrew`. + ## Tests Each module has a corresponding unit test target. These can be run individually, or all at once via the `UnitTests` scheme. diff --git a/README.md b/README.md index 47004ef0d3..76ad0049de 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,9 @@ Versions 4.9.6 and below use outdated SSL certificates and are unsupported. ## Demo +### Prerequisites +Our Xcode project uses SwiftLint, see [DEVELOPMENT.md](https://github.com/braintree/braintree_ios/blob/main/DEVELOPMENT.md#swiftlint) for installation instructions + 1. Run `pod install` * There is a known M1 mac issue with CocoaPods. See [this solution](https://github.com/CocoaPods/CocoaPods/issues/10220#issuecomment-730963835) to resolve `ffi` dependency issues. 2. Resolve the Swift Package Manager packages if needed: `File` > `Packages` > `Resolve Package Versions` or by running `swift package resolve` in Terminal From b0e5284f80c3a54263b1c9ab5a08f40afd69ba8c Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Mon, 15 Jul 2024 11:29:40 -0500 Subject: [PATCH 6/8] update demo app steps --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 76ad0049de..0b983dc76e 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,12 @@ Versions 4.9.6 and below use outdated SSL certificates and are unsupported. ## Demo -### Prerequisites -Our Xcode project uses SwiftLint, see [DEVELOPMENT.md](https://github.com/braintree/braintree_ios/blob/main/DEVELOPMENT.md#swiftlint) for installation instructions - +1. Our Xcode project uses SwiftLint. To ensure you have it installed see [DEVELOPMENT.md](https://github.com/braintree/braintree_ios/blob/main/DEVELOPMENT.md#swiftlint) 1. Run `pod install` * There is a known M1 mac issue with CocoaPods. See [this solution](https://github.com/CocoaPods/CocoaPods/issues/10220#issuecomment-730963835) to resolve `ffi` dependency issues. -2. Resolve the Swift Package Manager packages if needed: `File` > `Packages` > `Resolve Package Versions` or by running `swift package resolve` in Terminal -3. Open `Braintree.xcworkspace` in Xcode -4. Select the `Demo` scheme, and then run +1. Resolve the Swift Package Manager packages if needed: `File` > `Packages` > `Resolve Package Versions` or by running `swift package resolve` in Terminal +1. Open `Braintree.xcworkspace` in Xcode +1. Select the `Demo` scheme, and then run Xcode 15.0+ is required to run the demo app. From 56251230fc54f9614a6c1f218e07350395d88f4b Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Mon, 15 Jul 2024 12:56:47 -0500 Subject: [PATCH 7/8] Update .swiftlint.yml Co-authored-by: agedd <105314544+agedd@users.noreply.github.com> --- .swiftlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 66042ee3d8..fba09584bb 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -11,7 +11,7 @@ excluded: disabled_rules: - todo - - type_name # tests will have have the format _Tests + - type_name # tests will have the format _Tests - xctfail_message - blanket_disable_command - non_optional_string_data_conversion From 1f6c16aff358297bf36f99628367a9fbcc981cc9 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Mon, 15 Jul 2024 13:27:31 -0500 Subject: [PATCH 8/8] Update .swiftlint.yml --- .swiftlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index fba09584bb..998ab2c849 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,6 +1,6 @@ # Reference: https://github.com/realm/SwiftLint # Required Swiftlint Version -# swiftlint_version: 0.39.2 +# swiftlint_version: 0.55.1 # Paths to include in lint included: