From 323d24f2c98228eb62c4e469bcc8a5293d1f2117 Mon Sep 17 00:00:00 2001 From: Mohammad Fathi Date: Wed, 1 May 2024 16:11:14 -0700 Subject: [PATCH 1/3] Implements new TokenManager and KeychainUtility --- .../AuthorizationCodeAuthProvider.swift | 15 +- Sources/UberAuth/Client.swift | 50 ++--- .../Authenticators/UberAuthenticating.swift | 2 +- .../UberCore/Authentication/LoginButton.swift | 12 +- .../Authentication/LoginManager.swift | 4 +- .../LoginManagingProtocol.swift | 2 +- .../Authentication/Tokens/AccessToken.swift | 142 +++--------- .../Tokens/AccessTokenFactory.swift | 8 +- .../Tokens/AccessToken_DEPRECATED.swift | 148 +++++++++++++ .../Tokens/KeychainUtility.swift | 184 ++++++++++++++++ .../Authentication/Tokens/TokenManager.swift | 204 ++++++------------ .../Tokens/TokenManager_DEPRECATED.swift | 193 +++++++++++++++++ Sources/UberRides/RideRequestView.swift | 22 +- .../UberRides/RideRequestViewController.swift | 14 +- Sources/UberRides/RidesClient.swift | 8 +- .../UberSDK/UberSDK.xcodeproj/project.pbxproj | 61 ++---- examples/UberSDK/UberSDK/ContentView.swift | 2 +- examples/UberSDK/UberSDK/Info.plist | 6 +- .../{UberAuthMocks.swift => Mocks.swift} | 72 ++++++- .../AuthorizationCodeAuthProviderTests.swift | 8 +- .../UberCore/KeychainUtilityTests.swift | 77 +++++++ .../UberCore/TokenManagerTests.swift | 188 ++++++++++++++++ 22 files changed, 1056 insertions(+), 366 deletions(-) create mode 100644 Sources/UberCore/Authentication/Tokens/AccessToken_DEPRECATED.swift create mode 100644 Sources/UberCore/Authentication/Tokens/KeychainUtility.swift create mode 100644 Sources/UberCore/Authentication/Tokens/TokenManager_DEPRECATED.swift rename examples/UberSDK/UberSDKTests/Mocks/{UberAuthMocks.swift => Mocks.swift} (70%) create mode 100644 examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift create mode 100644 examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift diff --git a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift index b1e37fe7..e1ad4103 100644 --- a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift +++ b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift @@ -361,12 +361,15 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { fileprivate extension Client { init(tokenResponse: TokenRequest.Response) { - self = .init( - accessToken: tokenResponse.accessToken, - refreshToken: tokenResponse.refreshToken, - tokenType: tokenResponse.tokenType, - expiresIn: tokenResponse.expiresIn, - scope: tokenResponse.scope + self = Client( + authorizationCode: nil, + accessToken: AccessToken( + accessToken: tokenResponse.accessToken, + refreshToken: tokenResponse.refreshToken, + tokenType: tokenResponse.tokenType, + expiresIn: tokenResponse.expiresIn, + scope: tokenResponse.scope + ) ) } } diff --git a/Sources/UberAuth/Client.swift b/Sources/UberAuth/Client.swift index 18ff480b..57e5cd98 100644 --- a/Sources/UberAuth/Client.swift +++ b/Sources/UberAuth/Client.swift @@ -1,9 +1,30 @@ // -// Copyright © Uber Technologies, Inc. All rights reserved. +// Client.swift +// UberAuth // +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import Foundation +import UberCore public struct Client: Equatable { @@ -11,30 +32,14 @@ public struct Client: Equatable { public let authorizationCode: String? - public let accessToken: String? - - public let refreshToken: String? - - public let tokenType: String? - - public let expiresIn: Int? - - public let scope: [String]? + public let accessToken: AccessToken? // MARK: Initializers public init(authorizationCode: String? = nil, - accessToken: String? = nil, - refreshToken: String? = nil, - tokenType: String? = nil, - expiresIn: Int? = nil, - scope: [String]? = nil) { + accessToken: AccessToken? = nil) { self.authorizationCode = authorizationCode self.accessToken = accessToken - self.refreshToken = refreshToken - self.tokenType = tokenType - self.expiresIn = expiresIn - self.scope = scope } } @@ -43,11 +48,8 @@ extension Client: CustomStringConvertible { public var description: String { return """ Authorization Code: \(authorizationCode ?? "nil") - Access Token: \(accessToken ?? "nil") - Refresh Token: \(refreshToken ?? "nil") - Token Type: \(tokenType ?? "nil") - Expires In: \(expiresIn ?? -1) - Scopes: \(scope?.joined(separator: ", ") ?? "nil") + Access Token: + \(accessToken?.description ?? "nil") """ } } diff --git a/Sources/UberCore/Authentication/Authenticators/UberAuthenticating.swift b/Sources/UberCore/Authentication/Authenticators/UberAuthenticating.swift index 2949209e..16f346e0 100644 --- a/Sources/UberCore/Authentication/Authenticators/UberAuthenticating.swift +++ b/Sources/UberCore/Authentication/Authenticators/UberAuthenticating.swift @@ -24,7 +24,7 @@ import UIKit -public typealias AuthenticationCompletionHandler = (_ accessToken: AccessToken?, _ error: NSError?) -> Void +public typealias AuthenticationCompletionHandler = (_ accessToken: AccessToken_DEPRECATED?, _ error: NSError?) -> Void /** * Protocol to conform to for defining an authorization flow. diff --git a/Sources/UberCore/Authentication/LoginButton.swift b/Sources/UberCore/Authentication/LoginButton.swift index 5c4f7820..3898de94 100644 --- a/Sources/UberCore/Authentication/LoginButton.swift +++ b/Sources/UberCore/Authentication/LoginButton.swift @@ -49,7 +49,7 @@ public protocol LoginButtonDelegate: AnyObject { - parameter accessToken: The access token that - parameter error: The error that occured */ - func loginButton(_ button: LoginButton, didCompleteLoginWithToken accessToken: AccessToken?, error: NSError?) + func loginButton(_ button: LoginButton, didCompleteLoginWithToken accessToken: AccessToken_DEPRECATED?, error: NSError?) } /** @@ -87,7 +87,7 @@ public class LoginButton: UberButton { /// The current LoginButtonState of this button (signed in / signed out) public var buttonState: LoginButtonState { - if let _ = TokenManager.fetchToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) { + if let _ = TokenManager_DEPRECATED.fetchToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) { return .signedIn } else { return .signedOut @@ -102,7 +102,7 @@ public class LoginButton: UberButton { return loginManager.keychainAccessGroup } - private var loginCompletion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)? + private var loginCompletion: ((_ accessToken: AccessToken_DEPRECATED?, _ error: NSError?) -> Void)? public init(frame: CGRect, scopes: [UberScope], loginManager: LoginManager) { self.loginManager = loginManager @@ -129,8 +129,8 @@ public class LoginButton: UberButton { */ override public func setup() { super.setup() - NotificationCenter.default.addObserver(self, selector: #selector(refreshContent), name: Notification.Name(rawValue: TokenManager.tokenManagerDidSaveTokenNotification), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(refreshContent), name: Notification.Name(rawValue: TokenManager.tokenManagerDidDeleteTokenNotification), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(refreshContent), name: Notification.Name(rawValue: TokenManager_DEPRECATED.tokenManagerDidSaveTokenNotification), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(refreshContent), name: Notification.Name(rawValue: TokenManager_DEPRECATED.tokenManagerDidDeleteTokenNotification), object: nil) addTarget(self, action: #selector(uberButtonTapped), for: .touchUpInside) loginCompletion = { token, error in self.delegate?.loginButton(self, didCompleteLoginWithToken: token, error: error) @@ -212,7 +212,7 @@ public class LoginButton: UberButton { @objc func uberButtonTapped(_ button: UIButton) { switch buttonState { case .signedIn: - let success = TokenManager.deleteToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) + let success = TokenManager_DEPRECATED.deleteToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) delegate?.loginButton(self, didLogoutWithSuccess: success) refreshContent() case .signedOut: diff --git a/Sources/UberCore/Authentication/LoginManager.swift b/Sources/UberCore/Authentication/LoginManager.swift index 807fd25a..0e150513 100644 --- a/Sources/UberCore/Authentication/LoginManager.swift +++ b/Sources/UberCore/Authentication/LoginManager.swift @@ -460,7 +460,7 @@ public class LoginManager: LoginManaging, Identifiable, Equatable { authenticator = nil } - func loginCompletion(accessToken: AccessToken?, error: NSError?) { + func loginCompletion(accessToken: AccessToken_DEPRECATED?, error: NSError?) { loggingIn = false willEnterForegroundCalled = false authenticator = nil @@ -470,7 +470,7 @@ public class LoginManager: LoginManaging, Identifiable, Equatable { if let accessToken = accessToken { let tokenIdentifier = accessTokenIdentifier let accessGroup = keychainAccessGroup - let success = TokenManager.save(accessToken: accessToken, tokenIdentifier: tokenIdentifier, accessGroup: accessGroup) + let success = TokenManager_DEPRECATED.save(accessToken: accessToken, tokenIdentifier: tokenIdentifier, accessGroup: accessGroup) if !success { error = UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .unableToSaveAccessToken) print("Error: access token failed to save to keychain") diff --git a/Sources/UberCore/Authentication/LoginManagingProtocol.swift b/Sources/UberCore/Authentication/LoginManagingProtocol.swift index 330f6964..56281bcb 100644 --- a/Sources/UberCore/Authentication/LoginManagingProtocol.swift +++ b/Sources/UberCore/Authentication/LoginManagingProtocol.swift @@ -47,7 +47,7 @@ public protocol LoginManaging { - parameter prefillValues: Optional values to pre-populate the signin form with. - parameter completion: The LoginManagerRequestTokenHandler completion handler for login success/failure. */ - func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController?, prefillValues: Prefill?, completion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)?) + func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController?, prefillValues: Prefill?, completion: ((_ accessToken: AccessToken_DEPRECATED?, _ error: NSError?) -> Void)?) /** Called via the RidesAppDelegate when the application is opened via a URL. Responsible diff --git a/Sources/UberCore/Authentication/Tokens/AccessToken.swift b/Sources/UberCore/Authentication/Tokens/AccessToken.swift index 13dfd234..e7d90577 100644 --- a/Sources/UberCore/Authentication/Tokens/AccessToken.swift +++ b/Sources/UberCore/Authentication/Tokens/AccessToken.swift @@ -1,8 +1,8 @@ // // AccessToken.swift -// UberRides +// UberAuth // -// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,127 +22,45 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. + import Foundation -/** - Stores information about an access token used for authorizing requests. - - This class implements NSCoding, but its representation is an internal representation - not compatible with the OAuth representation. Use an initializer if you want to serialize this - via an OAuth representation. - */ -public class AccessToken: NSObject, NSCoding { - /// String containing the bearer token. - public private(set) var tokenString: String +public struct AccessToken: Codable, Equatable { - /// String containing the refresh token. - public private(set) var refreshToken: String? + public let accessToken: String? - /// String containing the token type. - public private(set) var tokenType: String? + public let refreshToken: String? - /// The expiration date for this access token - public private(set) var expirationDate: Date? + public let tokenType: String? - /// The scopes this token is valid for - public private(set) var grantedScopes: [UberScope] = [] + public let expiresIn: Int? - /** - Initializes an AccessToken with the provided tokenString - - - parameter tokenString: The access token string - */ - public init(tokenString: String) { - self.tokenString = tokenString - super.init() - } + public let scope: [String]? + + // MARK: Initializers - /** - Initializes an AccessToken with the provided parameters - - - parameter tokenString: The access token string - - parameter refreshToken: String containing the refresh token. - - parameter tokenType: String containing the token type. - - parameter expirationDate: The expiration date for this access token - - parameter grantedScopes: The scopes this token is valid for - */ - public init(tokenString: String, - refreshToken: String?, - tokenType: String?, - expirationDate: Date?, - grantedScopes: [UberScope]) { - self.tokenString = tokenString + public init(accessToken: String? = nil, + refreshToken: String? = nil, + tokenType: String? = nil, + expiresIn: Int? = nil, + scope: [String]? = nil) { + self.accessToken = accessToken self.refreshToken = refreshToken self.tokenType = tokenType - self.expirationDate = expirationDate - self.grantedScopes = grantedScopes - super.init() - } - - /** - Initializes an AccessToken using a dictionary with key/values matching - the OAuth access token response. - - See https://tools.ietf.org/html/rfc6749#section-5.1 for more details. - The `token_type` parameter is not required for this initializer, however is supported. - - - parameter oauthDictionary: A dictionary with key/values matching - the OAuth access token response. - */ - public init?(oauthDictionary: [String: Any]) { - guard let tokenString = oauthDictionary["access_token"] as? String else { return nil } - self.tokenString = tokenString - self.refreshToken = oauthDictionary["refresh_token"] as? String - self.tokenType = oauthDictionary["token_type"] as? String - if let expiresIn = oauthDictionary["expires_in"] as? Double { - self.expirationDate = Date(timeIntervalSinceNow: expiresIn) - } else if let expiresIn = oauthDictionary["expires_in"] as? String, - let expiresInDouble = Double(expiresIn) { - self.expirationDate = Date(timeIntervalSinceNow: expiresInDouble) - } - self.grantedScopes = (oauthDictionary["scope"] as? String)?.toUberScopesArray() ?? [] - } - - // MARK: NSCoding methods. - - /** - Note for reference. It would be better if these NSCoding methods allowed for serialization/deserialization for JSON. - However, this is used for serializing to Keychain via NSKeyedArchiver, and would take work to maintain backwards compatibility - if this was changed. Also, the OAuth `expires_in` parameter is a relative seconds string, which can't be stored by itself. - */ - - /** - Initializer to build an accessToken from the provided NSCoder. Allows for - serialization of an AccessToken - - - parameter decoder: The NSCoder to decode the AcccessToken from - - - returns: An initialized AccessToken, or nil if something went wrong - */ - public required init?(coder decoder: NSCoder) { - guard let token = decoder.decodeObject(forKey: "tokenString") as? String else { - return nil - } - tokenString = token - refreshToken = decoder.decodeObject(forKey: "refreshToken") as? String - tokenType = decoder.decodeObject(forKey: "tokenType") as? String - expirationDate = decoder.decodeObject(forKey: "expirationDate") as? Date - if let scopesString = decoder.decodeObject(forKey: "grantedScopes") as? String { - grantedScopes = scopesString.toUberScopesArray() - } - super.init() + self.expiresIn = expiresIn + self.scope = scope } +} + +extension AccessToken: CustomStringConvertible { - /** - Encodes the AccessToken. Required to allow for serialization - - - parameter coder: The NSCoder to encode the access token on - */ - public func encode(with coder: NSCoder) { - coder.encode(self.tokenString, forKey: "tokenString") - coder.encode(self.refreshToken, forKey: "refreshToken") - coder.encode(self.tokenType, forKey: "tokenType") - coder.encode(self.expirationDate, forKey: "expirationDate") - coder.encode(self.grantedScopes.toUberScopeString(), forKey: "grantedScopes") + public var description: String { + return """ + Access Token: \(accessToken ?? "nil") + Refresh Token: \(refreshToken ?? "nil") + Token Type: \(tokenType ?? "nil") + Expires In: \(expiresIn ?? -1) + Scopes: \(scope?.joined(separator: ", ") ?? "nil") + """ } } diff --git a/Sources/UberCore/Authentication/Tokens/AccessTokenFactory.swift b/Sources/UberCore/Authentication/Tokens/AccessTokenFactory.swift index b4675060..a7a3449c 100644 --- a/Sources/UberCore/Authentication/Tokens/AccessTokenFactory.swift +++ b/Sources/UberCore/Authentication/Tokens/AccessTokenFactory.swift @@ -35,7 +35,7 @@ public class AccessTokenFactory { - parameter url: The URL to parse the token from - returns: An initialized AccessToken, or nil if one couldn't be created */ - public static func createAccessToken(fromRedirectURL redirectURL: URL) throws -> AccessToken { + public static func createAccessToken(fromRedirectURL redirectURL: URL) throws -> AccessToken_DEPRECATED { guard var components = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false) else { throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidResponse) } @@ -71,20 +71,20 @@ public class AccessTokenFactory { - parameter jsonData: The JSON Data to parse the token from - returns: An initialized AccessToken */ - public static func createAccessToken(fromJSONData jsonData: Data) throws -> AccessToken { + public static func createAccessToken(fromJSONData jsonData: Data) throws -> AccessToken_DEPRECATED { guard let responseDictionary = (try? JSONSerialization.jsonObject(with: jsonData, options: [])) as? [String: Any] else { throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidResponse) } return try createAccessToken(from: responseDictionary) } - private static func createAccessToken(from oauthResponseDictionary: [String: Any]) throws -> AccessToken { + private static func createAccessToken(from oauthResponseDictionary: [String: Any]) throws -> AccessToken_DEPRECATED { if let error = oauthResponseDictionary["error"] as? String { guard let error = UberAuthenticationErrorFactory.createRidesAuthenticationError(rawValue: error) else { throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidRequest) } throw error - } else if let token = AccessToken(oauthDictionary: oauthResponseDictionary) { + } else if let token = AccessToken_DEPRECATED(oauthDictionary: oauthResponseDictionary) { return token } throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidResponse) diff --git a/Sources/UberCore/Authentication/Tokens/AccessToken_DEPRECATED.swift b/Sources/UberCore/Authentication/Tokens/AccessToken_DEPRECATED.swift new file mode 100644 index 00000000..afbf5b4f --- /dev/null +++ b/Sources/UberCore/Authentication/Tokens/AccessToken_DEPRECATED.swift @@ -0,0 +1,148 @@ +// +// AccessToken.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/** + Stores information about an access token used for authorizing requests. + + This class implements NSCoding, but its representation is an internal representation + not compatible with the OAuth representation. Use an initializer if you want to serialize this + via an OAuth representation. + */ +public class AccessToken_DEPRECATED: NSObject, NSCoding { + /// String containing the bearer token. + public private(set) var tokenString: String + + /// String containing the refresh token. + public private(set) var refreshToken: String? + + /// String containing the token type. + public private(set) var tokenType: String? + + /// The expiration date for this access token + public private(set) var expirationDate: Date? + + /// The scopes this token is valid for + public private(set) var grantedScopes: [UberScope] = [] + + /** + Initializes an AccessToken with the provided tokenString + + - parameter tokenString: The access token string + */ + public init(tokenString: String) { + self.tokenString = tokenString + super.init() + } + + /** + Initializes an AccessToken with the provided parameters + + - parameter tokenString: The access token string + - parameter refreshToken: String containing the refresh token. + - parameter tokenType: String containing the token type. + - parameter expirationDate: The expiration date for this access token + - parameter grantedScopes: The scopes this token is valid for + */ + public init(tokenString: String, + refreshToken: String?, + tokenType: String?, + expirationDate: Date?, + grantedScopes: [UberScope]) { + self.tokenString = tokenString + self.refreshToken = refreshToken + self.tokenType = tokenType + self.expirationDate = expirationDate + self.grantedScopes = grantedScopes + super.init() + } + + /** + Initializes an AccessToken using a dictionary with key/values matching + the OAuth access token response. + + See https://tools.ietf.org/html/rfc6749#section-5.1 for more details. + The `token_type` parameter is not required for this initializer, however is supported. + + - parameter oauthDictionary: A dictionary with key/values matching + the OAuth access token response. + */ + public init?(oauthDictionary: [String: Any]) { + guard let tokenString = oauthDictionary["access_token"] as? String else { return nil } + self.tokenString = tokenString + self.refreshToken = oauthDictionary["refresh_token"] as? String + self.tokenType = oauthDictionary["token_type"] as? String + if let expiresIn = oauthDictionary["expires_in"] as? Double { + self.expirationDate = Date(timeIntervalSinceNow: expiresIn) + } else if let expiresIn = oauthDictionary["expires_in"] as? String, + let expiresInDouble = Double(expiresIn) { + self.expirationDate = Date(timeIntervalSinceNow: expiresInDouble) + } + self.grantedScopes = (oauthDictionary["scope"] as? String)?.toUberScopesArray() ?? [] + } + + // MARK: NSCoding methods. + + /** + Note for reference. It would be better if these NSCoding methods allowed for serialization/deserialization for JSON. + However, this is used for serializing to Keychain via NSKeyedArchiver, and would take work to maintain backwards compatibility + if this was changed. Also, the OAuth `expires_in` parameter is a relative seconds string, which can't be stored by itself. + */ + + /** + Initializer to build an accessToken from the provided NSCoder. Allows for + serialization of an AccessToken + + - parameter decoder: The NSCoder to decode the AcccessToken from + + - returns: An initialized AccessToken, or nil if something went wrong + */ + public required init?(coder decoder: NSCoder) { + guard let token = decoder.decodeObject(forKey: "tokenString") as? String else { + return nil + } + tokenString = token + refreshToken = decoder.decodeObject(forKey: "refreshToken") as? String + tokenType = decoder.decodeObject(forKey: "tokenType") as? String + expirationDate = decoder.decodeObject(forKey: "expirationDate") as? Date + if let scopesString = decoder.decodeObject(forKey: "grantedScopes") as? String { + grantedScopes = scopesString.toUberScopesArray() + } + super.init() + } + + /** + Encodes the AccessToken. Required to allow for serialization + + - parameter coder: The NSCoder to encode the access token on + */ + public func encode(with coder: NSCoder) { + coder.encode(self.tokenString, forKey: "tokenString") + coder.encode(self.refreshToken, forKey: "refreshToken") + coder.encode(self.tokenType, forKey: "tokenType") + coder.encode(self.expirationDate, forKey: "expirationDate") + coder.encode(self.grantedScopes.toUberScopeString(), forKey: "grantedScopes") + } +} diff --git a/Sources/UberCore/Authentication/Tokens/KeychainUtility.swift b/Sources/UberCore/Authentication/Tokens/KeychainUtility.swift new file mode 100644 index 00000000..3c2b104e --- /dev/null +++ b/Sources/UberCore/Authentication/Tokens/KeychainUtility.swift @@ -0,0 +1,184 @@ +// +// KeychainUtility.swift +// UberAuth +// +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation + +/// @mockable +public protocol KeychainUtilityProtocol { + + /// Saves an object in the on device keychain using the supplied `key` + /// + /// - Parameters: + /// - value: The object to save. Must conform to the Codable protocol. + /// - key: A string value used to identify the saved object + /// - Returns: A boolean indicating whether or not the save operation was successful + func save(_ value: V, for key: String) -> Bool + + /// Retrieves an object from the on device keychain using the supplied `key` + /// + /// - Parameters: + /// - key: The identifier string used when saving the object + /// - Returns: If found, an optional type conforming to the Codable protocol + func get(key: String) -> V? + + /// Removes the object from the on device keychain corresponding to the supplied `key` + /// + /// - Parameters: + /// - key: The identifier string used when saving the object + /// - Returns: A boolean indicating whether or not the delete operation was successful + func delete(key: String) -> Bool +} + +public final class KeychainUtility: KeychainUtilityProtocol { + + // MARK: Properties + + private let serviceName = "com.uber.uber-ios-sdk" + private let accessGroup: String + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + // MARK: Initializers + + public init(accessGroup: String = "") { + self.accessGroup = accessGroup + } + + // MARK: KeychainUtilityProtocol + + // MARK: Save + + /// Saves an object in the on device keychain using the supplied `key` + /// + /// - Parameters: + /// - value: The object to save. Must conform to the Codable protocol. + /// - key: A string value used to identify the saved object + /// - Returns: A boolean indicating whether or not the save operation was successful + public func save(_ value: V, for key: String) -> Bool { + guard let data = try? encoder.encode(value) else { + return false + } + + let valueData = NSData(data: data) + var attributes = attributes(for: key) + attributes[Attribute.accessible] = kSecAttrAccessibleWhenUnlocked + attributes[Attribute.valueData] = valueData + + var result: OSStatus = SecItemAdd( + attributes as CFDictionary, + nil + ) + + if result == errSecDuplicateItem { + result = SecItemUpdate( + attributes as CFDictionary, + [Attribute.valueData: valueData] as CFDictionary + ) + } + + return result == errSecSuccess + } + + // MARK: Get + + /// Retrieves an object from the on device keychain using the supplied `key` + /// + /// - Parameters: + /// - key: The identifier string used when saving the object + /// - Returns: If found, an optional type conforming to the Codable protocol + public func get(key: String) -> V? { + + var attributes = attributes(for: key) + attributes[Attribute.matchLimit] = kSecMatchLimitOne + attributes[Attribute.returnData] = kCFBooleanTrue + + var obj: AnyObject? + let result = SecItemCopyMatching( + attributes as CFDictionary, + UnsafeMutablePointer(&obj) + ) + + guard result == noErr else { + return nil + } + + guard let data = obj as? Data, + let value = try? decoder.decode(V.self, from: data) else { + return nil + } + + return value + } + + // MARK: Delete + + /// Removes the object from the on device keychain corresponding to the supplied `key` + /// + /// - Parameters: + /// - key: The identifier string used when saving the object + /// - Returns: A boolean indicating whether or not the delete operation was successful + public func delete(key: String) -> Bool { + SecItemDelete( + attributes(for: key) as CFDictionary + ) == noErr + } + + // MARK: Private + + /// Builds a base set of attributes used to perform a keychain storage operation + /// - Parameter key: The object identifier + /// - Returns: A dictionary containing the attributes + private func attributes(for key: String) -> [String: Any] { + + let identifier = key.data(using: .utf8) + + var itemData = [String: Any]() + itemData[Attribute.generic] = identifier as AnyObject + itemData[Attribute.account] = identifier as AnyObject + itemData[Attribute.service] = serviceName as AnyObject + itemData[Attribute.class] = kSecClassGenericPassword + + if !accessGroup.isEmpty { + itemData[Attribute.accessGroup] = accessGroup as AnyObject + } + + return itemData + } + + // MARK: Constants + + enum Attribute { + static let `class` = kSecClass as String + static let account = kSecAttrAccount as String + static let service = kSecAttrService as String + static let accessControl = kSecAttrAccessControl as String + static let accessGroup = kSecAttrAccessGroup as String + static let generic = kSecAttrGeneric as String + static let accessible = kSecAttrAccessible as String + static let returnData = kSecReturnData as String + static let valueData = kSecValueData as String + static let matchLimit = kSecMatchLimit as String + } +} diff --git a/Sources/UberCore/Authentication/Tokens/TokenManager.swift b/Sources/UberCore/Authentication/Tokens/TokenManager.swift index 1e3154a4..438ccd60 100644 --- a/Sources/UberCore/Authentication/Tokens/TokenManager.swift +++ b/Sources/UberCore/Authentication/Tokens/TokenManager.swift @@ -1,8 +1,8 @@ // // TokenManager.swift -// UberRides +// UberCore // -// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,171 +22,93 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation -/// Manager class for saving and deleting AccessTokens. Allows you to manage tokens based on token identifier & keychain access group -public class TokenManager { +import Foundation - public static let tokenManagerDidSaveTokenNotification = "TokenManagerDidSaveTokenNotification" - public static let tokenManagerDidDeleteTokenNotification = "TokenManagerDidDeleteTokenNotification" +/// @mockable +public protocol TokenManaging { - private static let keychainWrapper = KeychainWrapper() - - //MARK: Get + /// Saves the provided Access Token to the on device keychain using the supplied `identifier` + /// + /// - Parameters: + /// - token: The Access Token to save + /// - identifier: A string used to identify the Access Token upon retrieval + /// - Returns: A boolean indicating whether or not the save operation was successful + func saveToken(_ token: AccessToken, identifier: String) -> Bool - /** - Gets the AccessToken for the given tokenIdentifier and accessGroup. - - - parameter identifier: The token identifier string to use - - parameter accessGroup: The keychain access group to use - - - returns: An AccessToken, or nil if one wasn't found - */ - public static func fetchToken(identifier: String, accessGroup: String) -> AccessToken? { - keychainWrapper.setAccessGroup(accessGroup) - guard let token = keychainWrapper.getObjectForKey(identifier) as? AccessToken else { - return nil - } - return token - } + /// Retrieves an Access Token from the on device keychain + /// + /// - Parameter identifier: The identifier string used when saving the Access Token + /// - Returns: An optional Access Token if found + func getToken(identifier: String) -> AccessToken? - /** - Gets the AccessToken for the given tokenIdentifier. - Uses the default value for keychain access group, as defined by your Configuration. - - - parameter tokenIdentifier: The token identifier string to use - - - returns: An AccessToken, or nil if one wasn't found - */ - public static func fetchToken(identifier: String) -> AccessToken? { - return self.fetchToken(identifier: identifier, - accessGroup: Configuration.shared.defaultKeychainAccessGroup) - } - /** - Gets the AccessToken using the default tokenIdentifier and accessGroup. - These values are the defined in your Configuration - - - returns: An AccessToken, or nil if one wasn't found - */ - public static func fetchToken() -> AccessToken? { - return self.fetchToken(identifier: Configuration.shared.defaultAccessTokenIdentifier, - accessGroup: Configuration.shared.defaultKeychainAccessGroup) - } + /// Removes the Access Token corresponding with the supplied `identifier` + /// + /// - Parameter identifier: The identifier string used when saving the Access Token + /// - Returns: A boolean indicating whether or not the delete operation was successful + func deleteToken(identifier: String) -> Bool +} - //MARK: Save +public final class TokenManager: TokenManaging { - /** - Saves the given AccessToken using the provided tokenIdentifier and acessGroup.If no values - are supplied, it uses the defaults defined in your Configuration. + public static let defaultAccessTokenIdentifier = "UberAccessTokenKey" - Access Token is saved syncronously - - - parameter accessToken: The AccessToken to save - - parameter tokenIdentifier: The token identifier string to use (defaults to Configuration.shared.defaultAccessTokenIdentifier) - - parameter accessGroup: The keychain access group to use (defaults to Configuration.shared.defaultKeychainAccessGroup) - - - returns: true if the accessToken was saved successfully, false otherwise - */ - @discardableResult public static func save(accessToken: AccessToken, tokenIdentifier: String, accessGroup: String) -> Bool { - keychainWrapper.setAccessGroup(accessGroup) - let success = keychainWrapper.setObject(accessToken, key: tokenIdentifier) - if success { - NotificationCenter.default.post(name: Notification.Name(rawValue: tokenManagerDidSaveTokenNotification), object: self) - } - return success - } + private let keychainUtility: KeychainUtilityProtocol - /** - Saves the given AccessToken using the provided tokenIdentifier. - Uses the default keychain access group defined by your Configuration. - - Access Token is saved syncronously - - - parameter accessToken: The AccessToken to save - - parameter tokenIdentifier: The token identifier string to use - - - returns: true if the accessToken was saved successfully, false otherwise - */ - @discardableResult public static func save(accessToken: AccessToken, tokenIdentifier: String) -> Bool { - return self.save(accessToken: accessToken, tokenIdentifier: tokenIdentifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) + public init(keychainUtility: KeychainUtilityProtocol = KeychainUtility()) { + self.keychainUtility = keychainUtility } - /** - Saves the given AccessToken. - Uses the default access token identifier & keychain access group defined by your - Configuration. - - Access Token is saved syncronously - - - parameter accessToken: The AccessToken to save - - - returns: true if the accessToken was saved successfully, false otherwise - */ - @discardableResult public static func save(accessToken: AccessToken) -> Bool { - return self.save(accessToken: accessToken, tokenIdentifier: Configuration.shared.defaultAccessTokenIdentifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) + // MARK: Save + + /// Saves the provided Access Token to the on device keychain using the supplied `identifier` + /// + /// - Parameters: + /// - token: The Access Token to save + /// - identifier: A string used to identify the Access Token upon retrieval + /// - Returns: A boolean indicating whether or not the save operation was successful + @discardableResult + public func saveToken(_ token: AccessToken, identifier: String = TokenManager.defaultAccessTokenIdentifier) -> Bool { + keychainUtility.save(token, for: identifier) } - //MARK: Delete + // MARK: Get - /** - Deletes the AccessToken for the givent tokenIdentifier and accessGroup. If no values - are supplied, it uses the defaults defined in your Configuration. - - - parameter tokenIdentifier: The token identifier string to use (defaults to Configuration.shared.defaultAccessTokenIdentifier) - - parameter accessGroup: The keychain access group to use (defaults to Configuration.shared.defaultKeychainAccessGroup) - - - returns: true if the token was deleted, false otherwise - */ - @discardableResult public static func deleteToken(identifier: String, accessGroup: String) -> Bool { - keychainWrapper.setAccessGroup(accessGroup) - deleteCookies() - let success = keychainWrapper.deleteObjectForKey(identifier) - if success { - NotificationCenter.default.post(name: Notification.Name(rawValue: tokenManagerDidDeleteTokenNotification), object: self) - } - return success + /// Retrieves an Access Token from the on device keychain + /// + /// - Parameter identifier: The identifier string used when saving the Access Token + /// - Returns: An optional Access Token if found + @discardableResult + public func getToken(identifier: String = TokenManager.defaultAccessTokenIdentifier) -> AccessToken? { + keychainUtility.get(key: identifier) } - /** - Deletes the AccessToken for the given tokenIdentifier. - Uses the default keychain access group defined in your Configuration. - - - parameter tokenIdentifier: The token identifier string to use - - - returns: true if the token was deleted, false otherwise - */ - @discardableResult public static func deleteToken(identifier: String) -> Bool { - return self.deleteToken(identifier: identifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) - } + // MARK: Delete - /** - Deletes an AccessToken. - Uses the default token identifier defined in your Configuration. - Uses the default keychain access group defined in your Configuration. - - - returns: true if the token was deleted, false otherwise - */ - @discardableResult public static func deleteToken() -> Bool { - return self.deleteToken(identifier: Configuration.shared.defaultAccessTokenIdentifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) + /// Removes the Access Token corresponding with the supplied `identifier` + /// + /// - Parameter identifier: The identifier string used when saving the Access Token + /// - Returns: A boolean indicating whether or not the delete operation was successful + @discardableResult + public func deleteToken(identifier: String = TokenManager.defaultAccessTokenIdentifier) -> Bool { + deleteCookies() + return keychainUtility.delete(key: identifier) } // MARK: Private Interface - private static func deleteCookies() { - Configuration.shared.resetProcessPool() - var urlsToClear = [URL]() - if let loginURL = URL(string: OAuth.regionHost) { - urlsToClear.append(loginURL) + /// Removes all cookies in the shared cookie store corresponding with the auth.uber.com domain + private func deleteCookies() { + guard let loginUrl = URL(string: OAuth.regionHost) else { + return } let sharedCookieStorage = HTTPCookieStorage.shared - for url in urlsToClear { - if let cookies = sharedCookieStorage.cookies(for: url) { - for cookie in cookies { - sharedCookieStorage.deleteCookie(cookie) - } + if let cookies = sharedCookieStorage.cookies(for: loginUrl) { + for cookie in cookies { + sharedCookieStorage.deleteCookie(cookie) } } } diff --git a/Sources/UberCore/Authentication/Tokens/TokenManager_DEPRECATED.swift b/Sources/UberCore/Authentication/Tokens/TokenManager_DEPRECATED.swift new file mode 100644 index 00000000..0cee83b7 --- /dev/null +++ b/Sources/UberCore/Authentication/Tokens/TokenManager_DEPRECATED.swift @@ -0,0 +1,193 @@ +// +// TokenManager.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Manager class for saving and deleting AccessTokens. Allows you to manage tokens based on token identifier & keychain access group +public class TokenManager_DEPRECATED { + + public static let tokenManagerDidSaveTokenNotification = "TokenManagerDidSaveTokenNotification" + public static let tokenManagerDidDeleteTokenNotification = "TokenManagerDidDeleteTokenNotification" + + private static let keychainWrapper = KeychainWrapper() + + //MARK: Get + + /** + Gets the AccessToken for the given tokenIdentifier and accessGroup. + + - parameter identifier: The token identifier string to use + - parameter accessGroup: The keychain access group to use + + - returns: An AccessToken, or nil if one wasn't found + */ + public static func fetchToken(identifier: String, accessGroup: String) -> AccessToken_DEPRECATED? { + keychainWrapper.setAccessGroup(accessGroup) + guard let token = keychainWrapper.getObjectForKey(identifier) as? AccessToken_DEPRECATED else { + return nil + } + return token + } + + /** + Gets the AccessToken for the given tokenIdentifier. + Uses the default value for keychain access group, as defined by your Configuration. + + - parameter tokenIdentifier: The token identifier string to use + + - returns: An AccessToken, or nil if one wasn't found + */ + public static func fetchToken(identifier: String) -> AccessToken_DEPRECATED? { + return self.fetchToken(identifier: identifier, + accessGroup: Configuration.shared.defaultKeychainAccessGroup) + } + + /** + Gets the AccessToken using the default tokenIdentifier and accessGroup. + These values are the defined in your Configuration + + - returns: An AccessToken, or nil if one wasn't found + */ + public static func fetchToken() -> AccessToken_DEPRECATED? { + return self.fetchToken(identifier: Configuration.shared.defaultAccessTokenIdentifier, + accessGroup: Configuration.shared.defaultKeychainAccessGroup) + } + + //MARK: Save + + /** + Saves the given AccessToken using the provided tokenIdentifier and acessGroup.If no values + are supplied, it uses the defaults defined in your Configuration. + + Access Token is saved syncronously + + - parameter accessToken: The AccessToken to save + - parameter tokenIdentifier: The token identifier string to use (defaults to Configuration.shared.defaultAccessTokenIdentifier) + - parameter accessGroup: The keychain access group to use (defaults to Configuration.shared.defaultKeychainAccessGroup) + + - returns: true if the accessToken was saved successfully, false otherwise + */ + @discardableResult public static func save(accessToken: AccessToken_DEPRECATED, tokenIdentifier: String, accessGroup: String) -> Bool { + keychainWrapper.setAccessGroup(accessGroup) + let success = keychainWrapper.setObject(accessToken, key: tokenIdentifier) + if success { + NotificationCenter.default.post(name: Notification.Name(rawValue: tokenManagerDidSaveTokenNotification), object: self) + } + return success + } + + /** + Saves the given AccessToken using the provided tokenIdentifier. + Uses the default keychain access group defined by your Configuration. + + Access Token is saved syncronously + + - parameter accessToken: The AccessToken to save + - parameter tokenIdentifier: The token identifier string to use + + - returns: true if the accessToken was saved successfully, false otherwise + */ + @discardableResult public static func save(accessToken: AccessToken_DEPRECATED, tokenIdentifier: String) -> Bool { + return self.save(accessToken: accessToken, tokenIdentifier: tokenIdentifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) + } + + /** + Saves the given AccessToken. + Uses the default access token identifier & keychain access group defined by your + Configuration. + + Access Token is saved syncronously + + - parameter accessToken: The AccessToken to save + + - returns: true if the accessToken was saved successfully, false otherwise + */ + @discardableResult public static func save(accessToken: AccessToken_DEPRECATED) -> Bool { + return self.save(accessToken: accessToken, tokenIdentifier: Configuration.shared.defaultAccessTokenIdentifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) + } + + //MARK: Delete + + /** + Deletes the AccessToken for the givent tokenIdentifier and accessGroup. If no values + are supplied, it uses the defaults defined in your Configuration. + + - parameter tokenIdentifier: The token identifier string to use (defaults to Configuration.shared.defaultAccessTokenIdentifier) + - parameter accessGroup: The keychain access group to use (defaults to Configuration.shared.defaultKeychainAccessGroup) + + - returns: true if the token was deleted, false otherwise + */ + @discardableResult public static func deleteToken(identifier: String, accessGroup: String) -> Bool { + keychainWrapper.setAccessGroup(accessGroup) + deleteCookies() + let success = keychainWrapper.deleteObjectForKey(identifier) + if success { + NotificationCenter.default.post(name: Notification.Name(rawValue: tokenManagerDidDeleteTokenNotification), object: self) + } + return success + } + + /** + Deletes the AccessToken for the given tokenIdentifier. + Uses the default keychain access group defined in your Configuration. + + - parameter tokenIdentifier: The token identifier string to use + + - returns: true if the token was deleted, false otherwise + */ + @discardableResult public static func deleteToken(identifier: String) -> Bool { + return self.deleteToken(identifier: identifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) + } + + /** + Deletes an AccessToken. + Uses the default token identifier defined in your Configuration. + Uses the default keychain access group defined in your Configuration. + + - returns: true if the token was deleted, false otherwise + */ + @discardableResult public static func deleteToken() -> Bool { + return self.deleteToken(identifier: Configuration.shared.defaultAccessTokenIdentifier, accessGroup: Configuration.shared.defaultKeychainAccessGroup) + } + + // MARK: Private Interface + + private static func deleteCookies() { + Configuration.shared.resetProcessPool() + var urlsToClear = [URL]() + if let loginURL = URL(string: OAuth.regionHost) { + urlsToClear.append(loginURL) + } + + let sharedCookieStorage = HTTPCookieStorage.shared + + for url in urlsToClear { + if let cookies = sharedCookieStorage.cookies(for: url) { + for cookie in cookies { + sharedCookieStorage.deleteCookie(cookie) + } + } + } + } +} diff --git a/Sources/UberRides/RideRequestView.swift b/Sources/UberRides/RideRequestView.swift index dc151471..d09955fb 100644 --- a/Sources/UberRides/RideRequestView.swift +++ b/Sources/UberRides/RideRequestView.swift @@ -53,7 +53,7 @@ public class RideRequestView: UIView { public var delegate: RideRequestViewDelegate? /// The access token used to authorize the web view - public var accessToken: AccessToken? + public var accessToken: AccessToken_DEPRECATED? /// Ther RideParameters to use for prefilling the RideRequestView public var rideParameters: RideParameters @@ -67,12 +67,12 @@ public class RideRequestView: UIView { Initializes to show the embedded Uber ride request view. - parameter rideParameters: The RideParameters to use for presetting values; defaults to using the current location for pickup - - parameter accessToken: specific access token to use with web view; defaults to using TokenManager's default token + - parameter accessToken: specific access token to use with web view; defaults to using TokenManager_DEPRECATED's default token - parameter frame: frame of the view. Defaults to CGRectZero - returns: An initialized RideRequestView */ - public required init(rideParameters: RideParameters, accessToken: AccessToken?, frame: CGRect) { + public required init(rideParameters: RideParameters, accessToken: AccessToken_DEPRECATED?, frame: CGRect) { self.rideParameters = rideParameters self.accessToken = accessToken let configuration = WKWebViewConfiguration() @@ -84,7 +84,7 @@ public class RideRequestView: UIView { /** Initializes to show the embedded Uber ride request view. - Uses the TokenManager's default accessToken + Uses the TokenManager_DEPRECATED's default accessToken - parameter rideParameters: The RideParameters to use for presetting values - parameter frame: frame of the view @@ -92,45 +92,45 @@ public class RideRequestView: UIView { - returns: An initialized RideRequestView */ public convenience init(rideParameters: RideParameters, frame: CGRect) { - self.init(rideParameters: rideParameters, accessToken: TokenManager.fetchToken(), frame: frame) + self.init(rideParameters: rideParameters, accessToken: TokenManager_DEPRECATED.fetchToken(), frame: frame) } /** Initializes to show the embedded Uber ride request view. Frame defaults to CGRectZero - Uses the TokenManager's default accessToken + Uses the TokenManager_DEPRECATED's default accessToken - parameter rideParameters: The RideParameters to use for presetting values - returns: An initialized RideRequestView */ public convenience init(rideParameters: RideParameters) { - self.init(rideParameters: rideParameters, accessToken: TokenManager.fetchToken(), frame: CGRect.zero) + self.init(rideParameters: rideParameters, accessToken: TokenManager_DEPRECATED.fetchToken(), frame: CGRect.zero) } /** Initializes to show the embedded Uber ride request view. Uses the current location for pickup - Uses the TokenManager's default accessToken + Uses the TokenManager_DEPRECATED's default accessToken - parameter frame: frame of the view - returns: An initialized RideRequestView */ public convenience override init(frame: CGRect) { - self.init(rideParameters: RideParametersBuilder().build(), accessToken: TokenManager.fetchToken(), frame: frame) + self.init(rideParameters: RideParametersBuilder().build(), accessToken: TokenManager_DEPRECATED.fetchToken(), frame: frame) } /** Initializes to show the embedded Uber ride request view. Uses the current location for pickup - Uses the TokenManager's default accessToken + Uses the TokenManager_DEPRECATED's default accessToken Frame defaults to CGRectZero - returns: An initialized RideRequestView */ public convenience init() { - self.init(rideParameters: RideParametersBuilder().build(), accessToken: TokenManager.fetchToken(), frame: CGRect.zero) + self.init(rideParameters: RideParametersBuilder().build(), accessToken: TokenManager_DEPRECATED.fetchToken(), frame: CGRect.zero) } required public init?(coder aDecoder: NSCoder) { diff --git a/Sources/UberRides/RideRequestViewController.swift b/Sources/UberRides/RideRequestViewController.swift index 0cd09a38..cae04424 100644 --- a/Sources/UberRides/RideRequestViewController.swift +++ b/Sources/UberRides/RideRequestViewController.swift @@ -61,7 +61,7 @@ public class RideRequestViewController: UIViewController { static let sourceString = "ride_request_widget" private var accessTokenWasUnauthorizedOnPreviousAttempt = false - private var loginCompletion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)? + private var loginCompletion: ((_ accessToken: AccessToken_DEPRECATED?, _ error: NSError?) -> Void)? /** Initializes a RideRequestViewController using the provided coder. By default, @@ -98,7 +98,7 @@ public class RideRequestViewController: UIViewController { rideParameters.source = rideParameters.source ?? RideRequestViewController.sourceString rideRequestView.rideParameters = rideParameters - rideRequestView.accessToken = TokenManager.fetchToken(identifier: loginManager.accessTokenIdentifier, accessGroup: loginManager.keychainAccessGroup) + rideRequestView.accessToken = TokenManager_DEPRECATED.fetchToken(identifier: loginManager.accessTokenIdentifier, accessGroup: loginManager.keychainAccessGroup) } // MARK: View Lifecycle @@ -130,7 +130,7 @@ public class RideRequestViewController: UIViewController { // MARK: Internal func load() { - if let accessToken = TokenManager.fetchToken(identifier: loginManager.accessTokenIdentifier, accessGroup: loginManager.keychainAccessGroup) { + if let accessToken = TokenManager_DEPRECATED.fetchToken(identifier: loginManager.accessTokenIdentifier, accessGroup: loginManager.keychainAccessGroup) { rideRequestView.accessToken = accessToken rideRequestView.load() } else { @@ -217,18 +217,18 @@ extension RideRequestViewController : RideRequestViewDelegate { private func attemptTokenRefresh() { let identifer = loginManager.accessTokenIdentifier let group = loginManager.keychainAccessGroup - guard let accessToken = TokenManager.fetchToken(identifier: identifer, accessGroup: group), let refreshToken = accessToken.refreshToken else { + guard let accessToken = TokenManager_DEPRECATED.fetchToken(identifier: identifer, accessGroup: group), let refreshToken = accessToken.refreshToken else { accessTokenWasUnauthorizedOnPreviousAttempt = true - _ = TokenManager.deleteToken(identifier: identifer, accessGroup: group) + _ = TokenManager_DEPRECATED.deleteToken(identifier: identifer, accessGroup: group) self.load() return } - _ = TokenManager.deleteToken(identifier: identifer, accessGroup: group) + _ = TokenManager_DEPRECATED.deleteToken(identifier: identifer, accessGroup: group) let ridesClient = RidesClient() ridesClient.refreshAccessToken(usingRefreshToken: refreshToken) { (accessToken, response) in if let token = accessToken { - _ = TokenManager.save(accessToken: token, tokenIdentifier: identifer, accessGroup: group) + _ = TokenManager_DEPRECATED.save(accessToken: token, tokenIdentifier: identifer, accessGroup: group) } self.load() } diff --git a/Sources/UberRides/RidesClient.swift b/Sources/UberRides/RidesClient.swift index ea9c6682..13bbc2bd 100644 --- a/Sources/UberRides/RidesClient.swift +++ b/Sources/UberRides/RidesClient.swift @@ -140,8 +140,8 @@ public class RidesClient { - returns: an AccessToken object, or nil if one can't be located */ - public func fetchAccessToken() -> AccessToken? { - guard let accessToken = TokenManager.fetchToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) else { + public func fetchAccessToken() -> AccessToken_DEPRECATED? { + guard let accessToken = TokenManager_DEPRECATED.fetchToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) else { return nil } return accessToken @@ -534,10 +534,10 @@ public class RidesClient { - parameter refreshToken: The Refresh Token String from an SSO access token - parameter completion: completion handler for the new access token */ - public func refreshAccessToken(usingRefreshToken refreshToken: String, completion:@escaping (_ accessToken: AccessToken?, _ response: Response) -> Void) { + public func refreshAccessToken(usingRefreshToken refreshToken: String, completion:@escaping (_ accessToken: AccessToken_DEPRECATED?, _ response: Response) -> Void) { let endpoint = OAuth.refresh(clientID: clientID, refreshToken: refreshToken) apiCall(endpoint) { response in - var accessToken: AccessToken? + var accessToken: AccessToken_DEPRECATED? if let data = response.data, response.error == nil { accessToken = try? AccessTokenFactory.createAccessToken(fromJSONData: data) diff --git a/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj b/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj index 7d1836ca..7e8fc2fd 100644 --- a/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj +++ b/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj @@ -7,13 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + B21E42782BE2FF6500D9C5E1 /* KeychainUtilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E42772BE2FF6500D9C5E1 /* KeychainUtilityTests.swift */; }; B28217C82B97A2E400EE786D /* AuthManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28217C72B97A2E400EE786D /* AuthManagerTests.swift */; }; B28A41712BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28A41702BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift */; }; B28A41752BAE331300F727C7 /* ParRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28A41742BAE331300F727C7 /* ParRequestTests.swift */; }; B28CDD322BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28CDD312BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift */; }; B28CDD342BA4BE2100EB1BBD /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28CDD332BA4BE2100EB1BBD /* RequestTests.swift */; }; B28CDD362BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28CDD352BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift */; }; - B2D096932B97B8E70093B510 /* UberAuthMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D096922B97B8E70093B510 /* UberAuthMocks.swift */; }; + B2983AE52BCF15FF00159EA7 /* TokenManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2983AE42BCF15FF00159EA7 /* TokenManagerTests.swift */; }; + B2983AE92BCF663100159EA7 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2983AE82BCF663100159EA7 /* Mocks.swift */; }; B2D096952B97C4A00093B510 /* UberAuthErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D096942B97C4A00093B510 /* UberAuthErrorTests.swift */; }; B2D0969A2B98012A0093B510 /* SelectionOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D096992B98012A0093B510 /* SelectionOptions.swift */; }; B2D7AE1C2B979EDA007F03FB /* UberSDKApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D7AE1B2B979EDA007F03FB /* UberSDKApp.swift */; }; @@ -35,16 +37,10 @@ remoteGlobalIDString = B2D7AE182B979EDA007F03FB; remoteInfo = UberSDK; }; - B2D7AE332B979EDB007F03FB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B293039A2B89113F00D28BAA /* Project object */; - proxyType = 1; - remoteGlobalIDString = B2D7AE182B979EDA007F03FB; - remoteInfo = UberSDK; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B21E42772BE2FF6500D9C5E1 /* KeychainUtilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainUtilityTests.swift; sourceTree = ""; }; B28217C72B97A2E400EE786D /* AuthManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManagerTests.swift; sourceTree = ""; }; B28A41702BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeResponseParserTests.swift; sourceTree = ""; }; B28A41742BAE331300F727C7 /* ParRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParRequestTests.swift; sourceTree = ""; }; @@ -53,7 +49,8 @@ B28CDD352BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizeRequestTests.swift; sourceTree = ""; }; B29303D62B891D3E00D28BAA /* SelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionView.swift; sourceTree = ""; }; B29303D82B89408D00D28BAA /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; - B2D096922B97B8E70093B510 /* UberAuthMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UberAuthMocks.swift; sourceTree = ""; }; + B2983AE42BCF15FF00159EA7 /* TokenManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenManagerTests.swift; sourceTree = ""; }; + B2983AE82BCF663100159EA7 /* Mocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; B2D096942B97C4A00093B510 /* UberAuthErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UberAuthErrorTests.swift; sourceTree = ""; }; B2D096982B9800020093B510 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; B2D096992B98012A0093B510 /* SelectionOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionOptions.swift; sourceTree = ""; }; @@ -83,13 +80,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B2D7AE2F2B979EDB007F03FB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -125,10 +115,19 @@ name = Products; sourceTree = ""; }; + B2983AE72BCF658E00159EA7 /* UberCore */ = { + isa = PBXGroup; + children = ( + B2983AE42BCF15FF00159EA7 /* TokenManagerTests.swift */, + B21E42772BE2FF6500D9C5E1 /* KeychainUtilityTests.swift */, + ); + path = UberCore; + sourceTree = ""; + }; B2D096912B97B8E70093B510 /* Mocks */ = { isa = PBXGroup; children = ( - B2D096922B97B8E70093B510 /* UberAuthMocks.swift */, + B2983AE82BCF663100159EA7 /* Mocks.swift */, ); path = Mocks; sourceTree = ""; @@ -160,6 +159,7 @@ isa = PBXGroup; children = ( B2D096912B97B8E70093B510 /* Mocks */, + B2983AE72BCF658E00159EA7 /* UberCore */, B28217C62B97A2CE00EE786D /* UberAuth */, ); path = UberSDKTests; @@ -225,10 +225,6 @@ CreatedOnToolsVersion = 15.0; TestTargetID = B2D7AE182B979EDA007F03FB; }; - B2D7AE312B979EDB007F03FB = { - CreatedOnToolsVersion = 15.0; - TestTargetID = B2D7AE182B979EDA007F03FB; - }; }; }; buildConfigurationList = B293039D2B89113F00D28BAA /* Build configuration list for PBXProject "UberSDK" */; @@ -270,13 +266,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B2D7AE302B979EDB007F03FB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -296,21 +285,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B21E42782BE2FF6500D9C5E1 /* KeychainUtilityTests.swift in Sources */, B28CDD342BA4BE2100EB1BBD /* RequestTests.swift in Sources */, B2D096952B97C4A00093B510 /* UberAuthErrorTests.swift in Sources */, B28217C82B97A2E400EE786D /* AuthManagerTests.swift in Sources */, B28CDD362BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift in Sources */, - B2D096932B97B8E70093B510 /* UberAuthMocks.swift in Sources */, B28A41752BAE331300F727C7 /* ParRequestTests.swift in Sources */, B28CDD322BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift in Sources */, B28A41712BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B2D7AE2E2B979EDB007F03FB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + B2983AE52BCF15FF00159EA7 /* TokenManagerTests.swift in Sources */, + B2983AE92BCF663100159EA7 /* Mocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -322,11 +306,6 @@ target = B2D7AE182B979EDA007F03FB /* UberSDK */; targetProxy = B2D7AE292B979EDB007F03FB /* PBXContainerItemProxy */; }; - B2D7AE342B979EDB007F03FB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B2D7AE182B979EDA007F03FB /* UberSDK */; - targetProxy = B2D7AE332B979EDB007F03FB /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ diff --git a/examples/UberSDK/UberSDK/ContentView.swift b/examples/UberSDK/UberSDK/ContentView.swift index a299d5f8..a75e4ee9 100644 --- a/examples/UberSDK/UberSDK/ContentView.swift +++ b/examples/UberSDK/UberSDK/ContentView.swift @@ -139,7 +139,7 @@ struct ContentView: View { .frame(maxWidth: .infinity, alignment: .leading) ScrollView(.horizontal) { Text(content.response ?? "") - .selectionDisabled(false) + .textSelection(.enabled) .padding() } } diff --git a/examples/UberSDK/UberSDK/Info.plist b/examples/UberSDK/UberSDK/Info.plist index e7721d82..1d4775a5 100644 --- a/examples/UberSDK/UberSDK/Info.plist +++ b/examples/UberSDK/UberSDK/Info.plist @@ -2,6 +2,10 @@ + UberClientID + 9QZcD_Ki6NbhGCrVXSUHCxfevm-C9Khj + UberDisplayName + Test CFBundleURLTypes @@ -22,7 +26,7 @@ UberAuth ClientID - <Client ID> + 9QZcD_Ki6NbhGCrVXSUHCxfevm-C9Khj RedirectURI com.uber.UberSDK://oauth/consumer diff --git a/examples/UberSDK/UberSDKTests/Mocks/UberAuthMocks.swift b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift similarity index 70% rename from examples/UberSDK/UberSDKTests/Mocks/UberAuthMocks.swift rename to examples/UberSDK/UberSDKTests/Mocks/Mocks.swift index 4eef5591..27fe1352 100644 --- a/examples/UberSDK/UberSDKTests/Mocks/UberAuthMocks.swift +++ b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift @@ -8,7 +8,7 @@ import AuthenticationServices import Foundation import UIKit @testable import UberAuth -import UberCore +@testable import UberCore class AuthorizationCodeResponseParsingMock: AuthorizationCodeResponseParsing { @@ -166,3 +166,73 @@ class AuthManagingMock: AuthManaging { } } +public class TokenManagingMock: TokenManaging { + public init() { } + + + public private(set) var saveTokenCallCount = 0 + public var saveTokenHandler: ((AccessToken, String) -> (Bool))? + public func saveToken(_ token: AccessToken, identifier: String) -> Bool { + saveTokenCallCount += 1 + if let saveTokenHandler = saveTokenHandler { + return saveTokenHandler(token, identifier) + } + return false + } + + public private(set) var getTokenCallCount = 0 + public var getTokenHandler: ((String) -> (AccessToken?))? + public func getToken(identifier: String) -> AccessToken? { + getTokenCallCount += 1 + if let getTokenHandler = getTokenHandler { + return getTokenHandler(identifier) + } + return nil + } + + public private(set) var deleteTokenCallCount = 0 + public var deleteTokenHandler: ((String) -> (Bool))? + public func deleteToken(identifier: String) -> Bool { + deleteTokenCallCount += 1 + if let deleteTokenHandler = deleteTokenHandler { + return deleteTokenHandler(identifier) + } + return false + } +} + +public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { + public init() { } + + + public private(set) var saveCallCount = 0 + public var saveHandler: ((Any, String) -> (Bool))? + public func save(_ value: V, for key: String) -> Bool { + saveCallCount += 1 + if let saveHandler = saveHandler { + return saveHandler(value, key) + } + return false + } + + public private(set) var getCallCount = 0 + public var getHandler: ((String) -> (Any?))? + public func get(key: String) -> V? { + getCallCount += 1 + if let getHandler = getHandler { + return getHandler(key) as? V + } + return nil + } + + public private(set) var deleteCallCount = 0 + public var deleteHandler: ((String) -> (Bool))? + public func delete(key: String) -> Bool { + deleteCallCount += 1 + if let deleteHandler = deleteHandler { + return deleteHandler(key) + } + return false + } +} + diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift index 86fe9cbc..a84e380c 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift @@ -478,9 +478,11 @@ final class AuthorizationCodeAuthProviderTests: XCTestCase { XCTAssertEqual( client, Client( - accessToken: "123", - tokenType: "test_token", - scope: [] + accessToken: AccessToken( + accessToken: "123", + tokenType: "test_token", + scope: [] + ) ) ) } diff --git a/examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift b/examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift new file mode 100644 index 00000000..d54f8e43 --- /dev/null +++ b/examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift @@ -0,0 +1,77 @@ +// +// KeychainUtility.swift +// UberAuth +// +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import XCTest +@testable import UberCore + +final class KeychainUtilityTests: XCTestCase { + + func test_save() { + let testObject = TestObject( + name: "test", + value: 5 + ) + + let keychainUtility = KeychainUtility() + + let saved = keychainUtility.save(testObject, for: "test_object") + XCTAssertTrue(saved) + } + + func test_get() { + let testObject = TestObject( + name: "test", + value: 5 + ) + + let keychainUtility = KeychainUtility() + + _ = keychainUtility.save(testObject, for: "test_object") + let retrievedObject: TestObject? = keychainUtility.get(key: "test_object") + + XCTAssertEqual(testObject, retrievedObject) + } + + func test_delete() { + let testObject = TestObject( + name: "test", + value: 5 + ) + + let keychainUtility = KeychainUtility() + + _ = keychainUtility.save(testObject, for: "test_object") + let deleted = keychainUtility.delete(key: "test_object") + XCTAssertTrue(deleted) + + let retrievedObject: TestObject? = keychainUtility.get(key: "test_object") + XCTAssertNil(retrievedObject) + } + + private struct TestObject: Codable, Equatable { + let name: String + let value: Int + } +} diff --git a/examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift b/examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift new file mode 100644 index 00000000..5e6bebc0 --- /dev/null +++ b/examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift @@ -0,0 +1,188 @@ +// +// KeychainUtility.swift +// UberAuth +// +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import XCTest +@testable import UberCore + +final class TokenManagerTests: XCTestCase { + + private let keychainUtility = KeychainUtilityProtocolMock() + + func test_saveToken_triggersKeychainUtilitySave() { + + keychainUtility.saveHandler = { _, identifier -> Bool in + XCTAssertEqual(identifier, "test_token_identifier") + return true + } + + let accessToken = AccessToken( + accessToken: "test_token_string" + ) + + let tokenManager = TokenManager( + keychainUtility: keychainUtility + ) + + XCTAssertEqual(keychainUtility.saveCallCount, 0) + + _ = tokenManager.saveToken(accessToken, identifier: "test_token_identifier") + + XCTAssertEqual(keychainUtility.saveCallCount, 1) + } + + func test_saveToken() { + let accessToken = AccessToken(accessToken: "test_token_string") + let tokenManager = TokenManager() + let result = tokenManager.saveToken(accessToken) + + XCTAssertTrue(result) + } + + func test_getToken_triggersKeychainUtilityGet() { + + let accessToken = AccessToken( + accessToken: "test_token_string" + ) + + keychainUtility.getHandler = { identifier -> AccessToken? in + XCTAssertEqual(identifier, "test_token_identifier") + return accessToken + } + + let tokenManager = TokenManager( + keychainUtility: keychainUtility + ) + + XCTAssertEqual(keychainUtility.getCallCount, 0) + + let token = tokenManager.getToken(identifier: "test_token_identifier") + + XCTAssertEqual(accessToken, token) + XCTAssertEqual(keychainUtility.getCallCount, 1) + } + + func test_getToken() { + let accessToken = AccessToken(accessToken: "test_token_string") + let tokenManager = TokenManager() + tokenManager.saveToken(accessToken) + + let token = tokenManager.getToken() + XCTAssertEqual(token, accessToken) + } + + func test_deleteToken_triggersKeychainUtilityDelete() { + + keychainUtility.deleteHandler = { identifier -> Bool in + XCTAssertEqual(identifier, "test_token_identifier") + return true + } + + let tokenManager = TokenManager( + keychainUtility: keychainUtility + ) + + XCTAssertEqual(keychainUtility.deleteCallCount, 0) + + tokenManager.deleteToken(identifier: "test_token_identifier") + + XCTAssertEqual(keychainUtility.deleteCallCount, 1) + } + + func test_deleteToken() { + let accessToken = AccessToken(accessToken: "test_token_string") + let tokenManager = TokenManager() + + tokenManager.saveToken(accessToken) + + let deleted = tokenManager.deleteToken() + XCTAssertTrue(deleted) + + let token = tokenManager.getToken() + XCTAssertNil(token) + } + + func test_deleteToken_noneSaved() { + let tokenManager = TokenManager() + + let deleted = tokenManager.deleteToken() + XCTAssertFalse(deleted) + + let token = tokenManager.getToken() + XCTAssertNil(token) + } + + func testCookiesCleared_whenTokenDeleted() { + guard let usUrl = URL(string: "https://login.uber.com") else { + XCTAssertFalse(false) + return + } + + let cookieStorage = HTTPCookieStorage.shared + if let cookies = cookieStorage.cookies { + for cookie in cookies { + cookieStorage.deleteCookie(cookie) + } + } + + cookieStorage.setCookies(createTestUSCookies(), for: usUrl, mainDocumentURL: nil) + UserDefaults.standard.synchronize() + + XCTAssertEqual(cookieStorage.cookies!.count, 2) + XCTAssertEqual(cookieStorage.cookies(for: usUrl)!.count, 2) + + let accessToken = AccessToken(accessToken: "test_token_string") + let tokenManager = TokenManager() + + tokenManager.saveToken(accessToken) + tokenManager.deleteToken() + + XCTAssertEqual(cookieStorage.cookies!.count, 0) + XCTAssertEqual(cookieStorage.cookies(for: usUrl)!.count, 0) + } + + // MARK: Helpers + + func createTestUSCookies() -> [HTTPCookie] { + let secureUSCookie = HTTPCookie( + properties: [HTTPCookiePropertyKey.domain: ".uber.com", + HTTPCookiePropertyKey.path : "/", + HTTPCookiePropertyKey.name : "us_login_secure", + HTTPCookiePropertyKey.value : "some_value", + HTTPCookiePropertyKey.secure : true] + ) + let unsecureUSCookie = HTTPCookie( + properties: [HTTPCookiePropertyKey.domain: ".uber.com", + HTTPCookiePropertyKey.path : "/", + HTTPCookiePropertyKey.name : "us_login_unecure", + HTTPCookiePropertyKey.value : "some_value", + HTTPCookiePropertyKey.secure : false] + ) + if let secureUSCookie = secureUSCookie, + let unsecureUSCookie = unsecureUSCookie { + return [secureUSCookie, unsecureUSCookie] + } + return [] + } +} From 0d3f42b494724fd493cf1b8a859a308cdc6ef543 Mon Sep 17 00:00:00 2001 From: Mohammad Fathi Date: Wed, 1 May 2024 16:11:35 -0700 Subject: [PATCH 2/3] Implements new UberButton and LoginButton --- Package.swift | 5 +- .../AuthorizationCodeAuthProvider.swift | 13 +- Sources/UberAuth/Button/LoginButton.swift | 151 +++++++++++ .../Resources/Media.xcassets/Contents.json | 6 + .../uber_logo_white.imageset/Contents.json | 23 ++ .../uber_logotype_white@1x.png | Bin 0 -> 1045 bytes .../uber_logotype_white@2x.png | Bin 0 -> 1897 bytes .../uber_logotype_white@3x.png | Bin 0 -> 2813 bytes .../UberCore/Authentication/LoginButton.swift | 253 ------------------ Sources/UberCore/Networking/Colors.swift | 27 ++ .../Resources/Media.xcassets/Contents.json | 6 + .../Contents.json | 38 +++ .../Contents.json | 38 +++ .../Contents.json | 38 +++ Sources/UberCore/UberButton.swift | 77 +++++- Sources/UberRides/RideRequestButton.swift | 2 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UberSDK/UberSDK.xcodeproj/project.pbxproj | 4 + examples/UberSDK/UberSDK/ContentView.swift | 11 + examples/UberSDK/UberSDK/UberButtonView.swift | 24 ++ 20 files changed, 465 insertions(+), 259 deletions(-) create mode 100644 Sources/UberAuth/Button/LoginButton.swift create mode 100644 Sources/UberAuth/Resources/Media.xcassets/Contents.json create mode 100644 Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/Contents.json create mode 100644 Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@1x.png create mode 100644 Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@2x.png create mode 100644 Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@3x.png delete mode 100644 Sources/UberCore/Authentication/LoginButton.swift create mode 100644 Sources/UberCore/Networking/Colors.swift create mode 100644 Sources/UberCore/Resources/Media.xcassets/Contents.json create mode 100644 Sources/UberCore/Resources/Media.xcassets/UberButtonBackground.colorset/Contents.json create mode 100644 Sources/UberCore/Resources/Media.xcassets/UberButtonForeground.colorset/Contents.json create mode 100644 Sources/UberCore/Resources/Media.xcassets/UberButtonHighlightedBackground.colorset/Contents.json create mode 100644 examples/Swift SDK/Swift SDK.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 examples/UberSDK/UberSDK/UberButtonView.swift diff --git a/Package.swift b/Package.swift index 0e0497ed..adb8d2d0 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "rides-ios-sdk", defaultLocalization: "en", platforms: [ - .iOS(.v13) + .iOS(.v15) ], products: [ .library( @@ -34,6 +34,9 @@ let package = Package( name: "UberAuth", dependencies: [ "UberCore" + ], + resources: [ + .process("Resources") ] ), .target( diff --git a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift index e1ad4103..71b8a14c 100644 --- a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift +++ b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift @@ -38,6 +38,8 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { private let networkProvider: NetworkProviding + private let tokenManager: TokenManaging + // MARK: Initializers public init(presentationAnchor: ASPresentationAnchor = .init(), @@ -60,6 +62,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.responseParser = AuthorizationCodeResponseParser() self.shouldExchangeAuthCode = shouldExchangeAuthCode self.networkProvider = NetworkProvider(baseUrl: Constants.baseUrl) + self.tokenManager = TokenManager() } init(presentationAnchor: ASPresentationAnchor = .init(), @@ -68,7 +71,8 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { configurationProvider: ConfigurationProviding = DefaultConfigurationProvider(), applicationLauncher: ApplicationLaunching = UIApplication.shared, responseParser: AuthorizationCodeResponseParsing = AuthorizationCodeResponseParser(), - networkProvider: NetworkProviding = NetworkProvider(baseUrl: Constants.baseUrl)) { + networkProvider: NetworkProviding = NetworkProvider(baseUrl: Constants.baseUrl), + tokenManager: TokenManaging = TokenManager()) { guard let clientID: String = configurationProvider.clientID else { preconditionFailure("No clientID specified in Info.plist") @@ -86,6 +90,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.responseParser = responseParser self.shouldExchangeAuthCode = shouldExchangeAuthCode self.networkProvider = networkProvider + self.tokenManager = tokenManager } // MARK: AuthProviding @@ -340,6 +345,12 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { switch result { case .success(let response): let client = Client(tokenResponse: response) + if let accessToken = client.accessToken { + self?.tokenManager.saveToken( + accessToken, + identifier: TokenManager.defaultAccessTokenIdentifier + ) + } completion(.success(client)) case .failure(let error): completion(.failure(error)) diff --git a/Sources/UberAuth/Button/LoginButton.swift b/Sources/UberAuth/Button/LoginButton.swift new file mode 100644 index 00000000..8e0b0d07 --- /dev/null +++ b/Sources/UberAuth/Button/LoginButton.swift @@ -0,0 +1,151 @@ +// +// Copyright © Uber Technologies, Inc. All rights reserved. +// + + +import Foundation +import UberCore +import UIKit + + +/// +/// A protocol to respond to Uber LoginButton events +/// +public protocol LoginButtonDelegate: AnyObject { + + /// + /// The login button attempted to log out + /// + /// - Parameters: + /// - button: The LoginButton instance that attempted logout + /// - success: A bollean indicating whether or not the logout was successful + func loginButton(_ button: LoginButton, + didLogoutWithSuccess success: Bool) + + + /// + /// The login button completed authentication + /// + /// - Parameters: + /// - button: The LoginButton instance that completed authentication + /// - result: A Result containing the authentication response. If successful, contains the Client object returned from the UberAuth authenticate function. If failed, contains an UberAuth error indicating the failure reason. + func loginButton(_ button: LoginButton, + didCompleteLoginWithResult result: Result) +} + +/// +/// A protocol to provide content for the Uber LoginButton +/// +public protocol LoginButtonDataSource: AnyObject { + + /// + /// Provides an optional AuthContext to be used during authentication + /// + /// - Parameter button: The LoginButton instance requesting the information + /// - Returns: An optional AuthContext instance + func authContext(_ button: LoginButton) -> AuthContext +} + +public final class LoginButton: UberButton { + + // MARK: Public Properties + + /// The LoginButtonDelegate for this button + public weak var delegate: LoginButtonDelegate? + + public weak var dataSource: LoginButtonDataSource? + + // MARK: Private Properties + + private var buttonState: State { + tokenManager.getToken() != nil ? .loggedIn : .loggedOut + } + + private let tokenManager = TokenManager() + + // MARK: Initializers + + public override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + // MARK: UberButton + + override public var title: String { + buttonState.title + } + + override public var image: UIImage? { + UIImage( + named: "uber_logo_white", + in: .module, + compatibleWith: nil + )?.withRenderingMode(.alwaysTemplate) + } + + // MARK: Private + + private func configure() { + addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + } + + @objc private func buttonTapped(_ sender: UIButton) { + switch buttonState { + case .loggedIn: + logout() + case .loggedOut: + login() + } + } + + private func login() { + let defaultContext = AuthContext( + authProvider: .authorizationCode( + shouldExchangeAuthCode: true + ) + ) + let context = dataSource?.authContext(self) ?? defaultContext + UberAuth.login(context: context) { [weak self] result in + guard let self else { return } + delegate?.loginButton(self, didCompleteLoginWithResult: result) + update() + } + } + + private func logout() { + // TODO: Implement UberAuth.logout() + tokenManager.deleteToken() + update() + } + + enum State { + case loggedIn + case loggedOut + + var title: String { + switch self { + case .loggedIn: + return NSLocalizedString( + "Sign Out", + bundle: .module, + comment: "Login Button Sign Out Description" + ) + .uppercased() + case .loggedOut: + return NSLocalizedString( + "Sign In", + bundle: .module, + comment: "Login Button Sign In Description" + ) + .uppercased() + } + } + } +} + diff --git a/Sources/UberAuth/Resources/Media.xcassets/Contents.json b/Sources/UberAuth/Resources/Media.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Sources/UberAuth/Resources/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/Contents.json b/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/Contents.json new file mode 100644 index 00000000..58c0c767 --- /dev/null +++ b/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "uber_logotype_white@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "uber_logotype_white@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "uber_logotype_white@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@1x.png b/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c5e46ad7fb2d58eafd27e0e217a9c770ee78ac0c GIT binary patch literal 1045 zcmV+w1nT>VP)Px&&q+i+ECqbf0tX4L7xtzk98J{d?_o_c>?Z zbDK)1)8jnPn+Ig|sZ{FJZ@2wlv;$$+_?cU1Yr-lto#AGJ!Qq@Ae`9t6*vTN_a1hNlLdGKw ziR!JkG)e9~1eLBj;>h&`ElEYJ{7?Lm;d*e3EKQR85y5U#BipND^x2I4Ezt-3Mk|)9 zJ8TH(2J~TSX=p50?E!w+dP4ND$X3gDxu>af-;wBTaqsb)tXQt@s=hw7=04?pt#h?R4jZm|}Wt2IZ1 z1q!goK#yCJ6;jLjXfW|7SN}v(fuub3C!y)(>sqM!GT$3Cip6%uG^VSyfkr;YXiTar z&QTMD#?!>vZcrr~aUe9`9MZEaUXNSW?-tg)oZYX?cm}>(GmG&1fdWuN!)w{3{Z>B8 z)rLL=s%cEzb`xYV>e03zZ`YK|kX1_P4*DX;H#Ht-=Ju1uZZc}lBN7w%@NdHcHf0K& z8qwGO4qa7raWy}qt3cy=9UzsHT}OQfXme$Az~GGMd0Sayo9|~f=%oo5hgrVM9d40& z^Gvcj*XC#v{0%mx9(Oz_0fWFp5M_`4(dJyG&QyC5`dCbS+~<}N??RKgSe+oRT4Eil z)2%pg;dKVA3<%HDRYESxtnn)8}% zQ;$>=HNP`P8uj$WFf>xoHlcr^sMj_|jRJ^LpVSMWjt-}xok&8*imc%mI&TUa(4y?; P00000NkvXXu0mjfftU5v literal 0 HcmV?d00001 diff --git a/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@2x.png b/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bc72863557ef841048582d2afef2aa487263cd67 GIT binary patch literal 1897 zcmV-v2bTDWP)Px+BS}O-RCodHn`@|4RTRg&d9x@}OS6a1u0{lfqKPONlwT?o!sy{6B=qPGi%S?bIy!&$C(BH zd-hp-t+m%)v(A2K5Cb429Ho4V!gVBm$1OM!`?@sy=s}lLVr_TnGQa~z*7z*O$4i^;^Bxxz$R8-QFziJGn!Zh zyS!Qq*e8ylXgbeXAUfn~iTxp+wYKkym8O03@KF47nRLeCBApl2sVxGGLOMitO~<_? zR*NH0@Z_|`D0(?az)nON^>Ppa9WGjQftan+*dDlt(PZF*61bUrLzN(J2Ll;w@J^V6c=Q4e5H26tb-SKN> z0_BtF+>0;&CeSUZ3jQlzvmHiT@bQr9sy%@;vZQ%|sUK_x!C0VRl#n`OOD~LzrDZ9T z$MulhKtlZC;f(adgT(tO>(J1mcS2^hCs5drq*n!OIKdsx52rWul_c$W=B0E%HY}QJ zF%$CiRl}lCw-y)CVc&JtNHGa~NfJFQMg?gFN&IaE+xKZ&XF`+kGrP4vS4JSc=h+L6 z7KnmAMd%Ck#=4Us_XXNdKp{Va_^j^$0I>`2EFiV9FOp5Pa#Aj>NMaId2?I#qwurV`Yw<8qw0`v{A2OKvXnhfs=gMS&kwe%-1c_bT7VFxH%3Dj3YDQJL0 zS3`O!DAAgMU>qY+KE?sj4J$PIRlcT zOF@g_u{I#)Q9zhLuJ=G%1e=ORBUu(n*MpncSw?PQb?$A&?yS*K&Xj6>R_#JJIKokV>af_;VU03c>vZ;z--*m^WcLvd z*-D9Qpy8J*)p&t+-z-p9&MJ*U*6F#n`Ok(8=OmDZFYqXpQ$Y58M<|@&+KSE+FcQ0I z>{%U2;Eq#8)AH^3c*ZGFnToIR>6=P4O}pD@_bo@;QyGN{Ptgc;1Jl74P%Irl;UzHG z;XqMr?EFxy10M3N$gBrCZb(Gv_5{9DIHE<9bu#Sq73=DcMFkHLT?R2U0GzN4!doRh zt*qN68A3?g-kU%Z&>X16t4?lpEUrT+o!)K-dSdkCle&ZTOh_Koj3aU~3F;Z5*gdL^>;5fRFOmi}0mCg4W2e6*~vLjKqYnITqH)7+bf8 ju{qD+*ID%l_($MBSHwyse~tQq00000NkvXXu0mjfQd5%{ literal 0 HcmV?d00001 diff --git a/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@3x.png b/Sources/UberAuth/Resources/Media.xcassets/uber_logo_white.imageset/uber_logotype_white@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a863a8b1c1ca541019c1d52431f87bf01922c92f GIT binary patch literal 2813 zcmVPxG1p-znrO=jO6%}lJRZ$V7K2UrFUj#9!7{en` zLIc`VO$D_n)j|;jHMIduq{JAlVAK>FL?K0rH7FKnX?v}J(b860xP@MS|I3+^bI#qf z^X=~Jp2wXp`R?w{eDB$BXLe@x+<}1zF#<6HrA1&+l1D~Hj^r>8JVe)RVE^FY;KDX_ zL5i{CbOfE6B5W@xg6S3Dgv`vz3?mc|gU^D`VDFA(vT955Q*EtwKCD{J ziQ2Xkm)(gq#zOp=2AyS0#_L9P9xF7shi+LjJcckJ2}ay@tR;svFK#kOq8#pku>4b&#ds^&Wm~uh)m=oL*4GWhCm)bo7HCkBGYEHjDF>g7r$S+vvR>EhxvzuBrAbt;7j* zYCkOlsAtm9@716M&Z8H*7C-NE#4*8KB$AI;5-c@VZXn=#XV81r)u8X?d=~U?MdzP# zD{N-enw$@|p-)59eu80q(!((b&?c`zklODwgTRkNQ%|rsL^MRK2nh|n5BwoAzHF59 zdLvyVt0)4r--D>q%=29am1j26!~|=MM!DzLA>J}XI=WmlqVzlwAj$NqV0lPt6jE=) zm|%ILor8x4(!0+62G*$tnn==_2>cPXPa9NEaWpZ(TqLSFK4wr;WD(XNK9b~(01rvM z(>`I~f4QTH3FabE&C!xP|Ek86s3m6v9yjpSXN_Zm8KM=CEq$53-c2j%|9J7rkLFLMXvRKq55uh!#9cr4voW~qZOfVOTfa-WA%=E|7N)Huy$;|AXcsXKPF_ z7m4KKcan>Pm#R_D;ifG8qt*n=^+c`@X%g!56zwKyC*zMkz3&UapV#5YB7-yc;&*+v z*q4n4d6A@G1lGepn?|WW^$N=C2KF2KA-PDZgTRx(#ROH0%!e9+ds8D#?7;3Ce67A0FO$e^>7W^0rE;XA#O)U9lY_mzUQO|Gfx#JMg7Pzvh-hcxAKF5Nx6_qSs zTZt^Qr6rru5%E7GxmL7ljtSNpkLbt>1r3$5fc$#^)VoC1 z4^0)98^G}dn*Ch}UQK(c_Z|Z-!4fN5sI^{#oMb^Qw0SR`J?;1{cDCcqTdprz7vp!K zJ%YqeiKg8g-vrYVP5am`1J4JGf;C?MK+z@mczaQWop#A11kyO7kxyQdgLf>%SbfMZr$PG`2=(qz*EJ#n0lVG z(4{2nSCEfG;2tn-^r7G2sDCWUc@k`;a1QZ$CaJQu%q7^9trcyql>nVguz$C)tE}Sd zLkp&1eogYMLGHU#WUIG(Gx&!Tnow11`Z&3MOOP~TSc{L|-kf%s39a5CHMDOB$51gJ z{6;&1#&S2wVZCuRty~J=oMdbLAq}i{rpd@sS7a?EP0!tFm9@HA2AU8ge-9y~3uS1y zJD0@zy(PJ1KXiKUy;0{LxSadd*VB!Pa0zDZ!QSXmT%=l0Y#&seV0(QA<*eUk;G1Hg z2|-epk0ER;B4dFyv09XQfBNTMab5K=Opa zNH=^pkVcZ02xxLbcpk4(*~ zoonFJn7SWevcZYx3^b7>IRg4Wa#~u+3s=Et8P#3Gz3V!uL|u)Qs}m`j3pI=Q`R1Y^ zSM!i^gh!HQI`8q&^h&i}lzpJJs0ba10LiH#;v$3UlcD)s8eP@|ldP|iwOTX{0?0ZM zL5mH-euLxc`uhRiW^lst1T~0>Bsn9X$qr3=_&~dF+K%cP6KoxlHu|JduNSXpdT!8d`L zE&Me2GSC8965k>(3(a-dUSwF>xEE1FVQqu5fQG}SodOnN9-686Xc@A4#F(qqp7-FN zcj4D8u_Qv-2waBvH69hpqh;V+58c230%w9dBsnMShi?}+1a7OHt1K^QFJMr>kgw)7 zrXs%ghj18M-&>KrS=2lZaed^J)lYb58Cg2_5;%_F}XdGyJLb|LgD+>C$f#d--Y$5@NaOUc5fQOcMElh*`5 zL%sKad6P_Hih_onpY6fMDB|}J%oi0A&;z_Hd%(sf6S)UBP;g6AVe-Wcikmuw!~_#> zEy-qcYFQr>ODNchpo_tC$60c1!RL44vyS}=>+$ceJ?h*-?6xdkFQ6AY%K6wf!9+kX zHiIAJRAbA{Dk~B6(t9&_0y_0?I4xarUUT;3Wh{YDe%=7;m!&oZmfuZ&%C(;Byk%{`oSLjTd?~u-gfhN>oPWamUGTNx zJ$ykTvJVGCwiV)X!!Vp$Gb#kgm}amb=p=_T7`@hbYEo}>-|nDe&T@oVFLHB0?dWi* z@u#NV4+N7yySASM9|Sd~)lgv_Plks%Ru1^^A(U#l;FVz0i)mnEG~KQS7wR2LehW90 z+)JZkbXIsahYP`J-~_M}pk7)(3UgHm<+R-y$eEcUOaHd_2PyTrtI+*Xn6;jnHu}?O zO#k0|b*= Prefill? -} - -/// Button to handle logging in to Uber -public class LoginButton: UberButton { - - let horizontalCenterPadding: CGFloat = 50 - let loginVerticalPadding: CGFloat = 15 - let loginHorizontalEdgePadding: CGFloat = 15 - - /// The LoginButtonDelegate for this button - public weak var delegate: LoginButtonDelegate? - - public weak var dataSource: LoginButtonDataSource? - - /// The LoginManager to use for log in - public var loginManager: LoginManager { - didSet { - refreshContent() - } - } - - /// The UberScopes to request - public var scopes: [UberScope] - - /// The view controller to present login over. Used - public var presentingViewController: UIViewController? - - /// The current LoginButtonState of this button (signed in / signed out) - public var buttonState: LoginButtonState { - if let _ = TokenManager_DEPRECATED.fetchToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) { - return .signedIn - } else { - return .signedOut - } - } - - private var accessTokenIdentifier: String { - return loginManager.accessTokenIdentifier - } - - private var keychainAccessGroup: String { - return loginManager.keychainAccessGroup - } - - private var loginCompletion: ((_ accessToken: AccessToken_DEPRECATED?, _ error: NSError?) -> Void)? - - public init(frame: CGRect, scopes: [UberScope], loginManager: LoginManager) { - self.loginManager = loginManager - self.scopes = scopes - super.init(frame: frame) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - loginManager = LoginManager(loginType: .native) - scopes = [] - super.init(coder: aDecoder) - setup() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - //Mark: UberButton - - /** - Setup the LoginButton by adding a target to the button and setting the login completion block - */ - override public func setup() { - super.setup() - NotificationCenter.default.addObserver(self, selector: #selector(refreshContent), name: Notification.Name(rawValue: TokenManager_DEPRECATED.tokenManagerDidSaveTokenNotification), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(refreshContent), name: Notification.Name(rawValue: TokenManager_DEPRECATED.tokenManagerDidDeleteTokenNotification), object: nil) - addTarget(self, action: #selector(uberButtonTapped), for: .touchUpInside) - loginCompletion = { token, error in - self.delegate?.loginButton(self, didCompleteLoginWithToken: token, error: error) - self.refreshContent() - } - sizeToFit() - } - - /** - Updates the content of the button. Sets the image icon and font, as well as the text - */ - override public func setContent() { - super.setContent() - - let buttonFont = UIFont.systemFont(ofSize: 13) - let titleText = titleForButtonState(buttonState) - let logo = getImage("ic_logo_white") - - - uberTitleLabel.font = buttonFont - uberTitleLabel.text = titleText - - uberImageView.image = logo - uberImageView.contentMode = .center - } - - /** - Adds the layout constraints for the Login button. - */ - override public func setConstraints() { - - uberTitleLabel.translatesAutoresizingMaskIntoConstraints = false - uberImageView.translatesAutoresizingMaskIntoConstraints = false - - uberImageView.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal) - uberTitleLabel.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal) - uberTitleLabel.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical) - - let imageLeftConstraint = NSLayoutConstraint(item: uberImageView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: loginHorizontalEdgePadding) - let imageTopConstraint = NSLayoutConstraint(item: uberImageView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: loginVerticalPadding) - let imageBottomConstraint = NSLayoutConstraint(item: uberImageView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -loginVerticalPadding) - - let titleLabelRightConstraint = NSLayoutConstraint(item: uberTitleLabel, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: -loginHorizontalEdgePadding) - let titleLabelCenterYConstraint = NSLayoutConstraint(item: uberTitleLabel, attribute: .centerY, relatedBy: .equal, toItem: uberImageView, attribute: .centerY, multiplier: 1.0, constant: 0.0) - - let imagePaddingRightConstraint = NSLayoutConstraint(item: uberTitleLabel, attribute: .left, relatedBy: .greaterThanOrEqual , toItem: uberImageView, attribute: .right, multiplier: 1.0, constant: imageLabelPadding) - - let horizontalCenterPaddingConstraint = NSLayoutConstraint(item: uberTitleLabel, attribute: .left, relatedBy: .greaterThanOrEqual , toItem: uberImageView, attribute: .right, multiplier: 1.0, constant: horizontalCenterPadding) - horizontalCenterPaddingConstraint.priority = UILayoutPriority.defaultLow - - addConstraints([imageLeftConstraint, imageTopConstraint, imageBottomConstraint]) - addConstraints([titleLabelRightConstraint, titleLabelCenterYConstraint]) - addConstraints([imagePaddingRightConstraint, horizontalCenterPaddingConstraint]) - } - - //Mark: UIView - - override public func sizeThatFits(_ size: CGSize) -> CGSize { - let sizeThatFits = super.sizeThatFits(size) - - let iconSizeThatFits = uberImageView.image?.size ?? CGSize.zero - let labelSizeThatFits = uberTitleLabel.intrinsicContentSize - - let labelMinHeight = labelSizeThatFits.height + 2 * loginVerticalPadding - let iconMinHeight = iconSizeThatFits.height + 2 * loginVerticalPadding - - let height = max(iconMinHeight, labelMinHeight) - - return CGSize(width: sizeThatFits.width + horizontalCenterPadding, height: height) - } - - override public func updateConstraints() { - refreshContent() - super.updateConstraints() - } - - //Mark: Internal Interface - - @objc func uberButtonTapped(_ button: UIButton) { - switch buttonState { - case .signedIn: - let success = TokenManager_DEPRECATED.deleteToken(identifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) - delegate?.loginButton(self, didLogoutWithSuccess: success) - refreshContent() - case .signedOut: - loginManager.login( - requestedScopes: scopes, - presentingViewController: presentingViewController, - prefillValues: dataSource?.prefillValues(self), - completion: loginCompletion - ) - } - } - - //Mark: Private Interface - - @objc private func refreshContent() { - DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return } - - strongSelf.uberTitleLabel.text = strongSelf.titleForButtonState(strongSelf.buttonState) - } - } - - private func titleForButtonState(_ buttonState: LoginButtonState) -> String { - var titleText: String! - switch buttonState { - case .signedIn: - titleText = NSLocalizedString("Sign Out", bundle: Bundle(for: type(of: self)), comment: "Login Button Sign Out Description").uppercased() - case .signedOut: - titleText = NSLocalizedString("Sign In", bundle: Bundle(for: type(of: self)), comment: "Login Button Sign In Description").uppercased() - } - return titleText - } - - private func getImage(_ name: String) -> UIImage? { - let bundle = Bundle(for: LoginButton.self) - return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) - } -} diff --git a/Sources/UberCore/Networking/Colors.swift b/Sources/UberCore/Networking/Colors.swift new file mode 100644 index 00000000..1e6dc424 --- /dev/null +++ b/Sources/UberCore/Networking/Colors.swift @@ -0,0 +1,27 @@ +// +// Copyright © Uber Technologies, Inc. All rights reserved. +// + + +import UIKit + +extension UIColor { + + static let uberButtonBackground: UIColor = UIColor( + named: "UberButtonBackground", + in: .module, + compatibleWith: nil + ) ?? UIColor.darkText + + static let uberButtonHighlightedBackground: UIColor = UIColor( + named: "UberButtonHighlightedBackground", + in: .module, + compatibleWith: nil + ) ?? UIColor.darkText + + static let uberButtonForeground: UIColor = UIColor( + named: "UberButtonForeground", + in: .module, + compatibleWith: nil + ) ?? UIColor.lightText +} diff --git a/Sources/UberCore/Resources/Media.xcassets/Contents.json b/Sources/UberCore/Resources/Media.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Sources/UberCore/Resources/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/UberCore/Resources/Media.xcassets/UberButtonBackground.colorset/Contents.json b/Sources/UberCore/Resources/Media.xcassets/UberButtonBackground.colorset/Contents.json new file mode 100644 index 00000000..0c600f92 --- /dev/null +++ b/Sources/UberCore/Resources/Media.xcassets/UberButtonBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/UberCore/Resources/Media.xcassets/UberButtonForeground.colorset/Contents.json b/Sources/UberCore/Resources/Media.xcassets/UberButtonForeground.colorset/Contents.json new file mode 100644 index 00000000..9c0e331e --- /dev/null +++ b/Sources/UberCore/Resources/Media.xcassets/UberButtonForeground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/UberCore/Resources/Media.xcassets/UberButtonHighlightedBackground.colorset/Contents.json b/Sources/UberCore/Resources/Media.xcassets/UberButtonHighlightedBackground.colorset/Contents.json new file mode 100644 index 00000000..8b2931b2 --- /dev/null +++ b/Sources/UberCore/Resources/Media.xcassets/UberButtonHighlightedBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x27", + "green" : "0x27", + "red" : "0x28" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE4", + "green" : "0xE5", + "red" : "0xE5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/UberCore/UberButton.swift b/Sources/UberCore/UberButton.swift index aec25bae..c22948c0 100644 --- a/Sources/UberCore/UberButton.swift +++ b/Sources/UberCore/UberButton.swift @@ -1,8 +1,8 @@ // // UberButton.swift -// UberRides +// UberCore // -// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,8 +24,79 @@ import UIKit -/// Base class for Uber buttons that sets up colors and some constraints. open class UberButton: UIButton { + + // MARK: Public Properties + + open var title: String { "" } + + open var image: UIImage? { nil } + + // MARK: Initializers + + public override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + // MARK: UIButton + + open override var isHighlighted: Bool { + didSet { update() } + } + + // MARK: Private + + private func configure() { + contentHorizontalAlignment = .fill + update() + } + + public func update() { + DispatchQueue.main.async { [self] in + configuration = .uber( + title: title, + image: image, + isHighlighted: isHighlighted + ) + updateConfiguration() + } + } +} + +extension UIButton.Configuration { + + static func uber(title: String? = nil, + image: UIImage? = nil, + isHighlighted: Bool = false) -> UIButton.Configuration { + + var style: UIButton.Configuration = .plain() + + // Background Color + var background = style.background + background.backgroundColor = isHighlighted ? .uberButtonHighlightedBackground : .uberButtonBackground + style.background = background + + // Image + style.image = image + style.imagePadding = 12.0 + + // Text + style.title = title + style.titleAlignment = .trailing + style.baseForegroundColor = .uberButtonForeground + + return style + } +} + +/// Base class for Uber buttons that sets up colors and some constraints. +open class UberButton_DEPRECATED: UIButton { public let cornerRadius: CGFloat = 8 public let horizontalEdgePadding: CGFloat = 16 public let imageLabelPadding: CGFloat = 8 diff --git a/Sources/UberRides/RideRequestButton.swift b/Sources/UberRides/RideRequestButton.swift index 105f9f92..4a4942ee 100644 --- a/Sources/UberRides/RideRequestButton.swift +++ b/Sources/UberRides/RideRequestButton.swift @@ -47,7 +47,7 @@ public protocol RideRequestButtonDelegate { } /// RequestButton implements a button on the touch screen to request a ride. -public class RideRequestButton: UberButton { +public class RideRequestButton: UberButton_DEPRECATED { /// Delegate is informed of events that occur with request button. public var delegate: RideRequestButtonDelegate? diff --git a/examples/Swift SDK/Swift SDK.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/Swift SDK/Swift SDK.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/examples/Swift SDK/Swift SDK.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj b/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj index 7e8fc2fd..5c16970b 100644 --- a/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj +++ b/examples/UberSDK/UberSDK.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ B21E42782BE2FF6500D9C5E1 /* KeychainUtilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E42772BE2FF6500D9C5E1 /* KeychainUtilityTests.swift */; }; + B25D308A2BD03F50008A67CF /* UberButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D30892BD03F50008A67CF /* UberButtonView.swift */; }; B28217C82B97A2E400EE786D /* AuthManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28217C72B97A2E400EE786D /* AuthManagerTests.swift */; }; B28A41712BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28A41702BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift */; }; B28A41752BAE331300F727C7 /* ParRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28A41742BAE331300F727C7 /* ParRequestTests.swift */; }; @@ -41,6 +42,7 @@ /* Begin PBXFileReference section */ B21E42772BE2FF6500D9C5E1 /* KeychainUtilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainUtilityTests.swift; sourceTree = ""; }; + B25D30892BD03F50008A67CF /* UberButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UberButtonView.swift; sourceTree = ""; }; B28217C72B97A2E400EE786D /* AuthManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManagerTests.swift; sourceTree = ""; }; B28A41702BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeResponseParserTests.swift; sourceTree = ""; }; B28A41742BAE331300F727C7 /* ParRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParRequestTests.swift; sourceTree = ""; }; @@ -140,6 +142,7 @@ B29303D62B891D3E00D28BAA /* SelectionView.swift */, B29303D82B89408D00D28BAA /* Helpers.swift */, B2D7AE1D2B979EDA007F03FB /* ContentView.swift */, + B25D30892BD03F50008A67CF /* UberButtonView.swift */, B2D096992B98012A0093B510 /* SelectionOptions.swift */, B2D7AE1F2B979EDB007F03FB /* Assets.xcassets */, B2D7AE212B979EDB007F03FB /* Preview Content */, @@ -278,6 +281,7 @@ B2D7AE492B979F58007F03FB /* SelectionView.swift in Sources */, B2D0969A2B98012A0093B510 /* SelectionOptions.swift in Sources */, B2D7AE4A2B979F5D007F03FB /* Helpers.swift in Sources */, + B25D308A2BD03F50008A67CF /* UberButtonView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/UberSDK/UberSDK/ContentView.swift b/examples/UberSDK/UberSDK/ContentView.swift index a75e4ee9..dfc88a12 100644 --- a/examples/UberSDK/UberSDK/ContentView.swift +++ b/examples/UberSDK/UberSDK/ContentView.swift @@ -152,9 +152,20 @@ struct ContentView: View { "Login", content: { loginSection } ) + Section( + "Uber Button", + content: { uberButtonSection } + ) } } + + @ViewBuilder + private var uberButtonSection: some View { + UberButtonView() + .padding() + } + @ViewBuilder private var loginSection: some View { diff --git a/examples/UberSDK/UberSDK/UberButtonView.swift b/examples/UberSDK/UberSDK/UberButtonView.swift new file mode 100644 index 00000000..7fc069f0 --- /dev/null +++ b/examples/UberSDK/UberSDK/UberButtonView.swift @@ -0,0 +1,24 @@ +// +// Copyright © Uber Technologies, Inc. All rights reserved. +// + + +import Foundation +import SwiftUI +import UberAuth +import UberCore + +struct UberButtonView: UIViewRepresentable { + func makeUIView(context: Context) -> UberCore.UberButton { + LoginButton() + } + + func updateUIView(_ uiView: UberCore.UberButton, context: Context) {} +} + +#Preview { + VStack { + UberButtonView() + .padding() + } +} From 1c373d8218ff741c5205ebfbb1bfbf63a7adfdb0 Mon Sep 17 00:00:00 2001 From: Mohammad Fathi Date: Fri, 3 May 2024 16:55:35 -0700 Subject: [PATCH 3/3] TokenManager, AccessToken changes --- .../AuthorizationCodeAuthProvider.swift | 2 +- Sources/UberAuth/Button/LoginButton.swift | 24 +++++++-- Sources/UberAuth/Token/TokenRequest.swift | 51 +------------------ .../Authentication/Tokens/AccessToken.swift | 42 +++++++++++++-- .../Tokens/KeychainUtility.swift | 8 +-- .../Authentication/Tokens/TokenManager.swift | 1 - .../UberSDK/UberSDKTests/Mocks/Mocks.swift | 4 +- .../AuthorizationCodeAuthProviderTests.swift | 9 ++-- .../UberCore/KeychainUtilityTests.swift | 24 ++++++--- .../UberCore/TokenManagerTests.swift | 32 ++++++++---- 10 files changed, 111 insertions(+), 86 deletions(-) diff --git a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift index 71b8a14c..edc2436a 100644 --- a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift +++ b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift @@ -375,7 +375,7 @@ fileprivate extension Client { self = Client( authorizationCode: nil, accessToken: AccessToken( - accessToken: tokenResponse.accessToken, + tokenString: tokenResponse.tokenString, refreshToken: tokenResponse.refreshToken, tokenType: tokenResponse.tokenType, expiresIn: tokenResponse.expiresIn, diff --git a/Sources/UberAuth/Button/LoginButton.swift b/Sources/UberAuth/Button/LoginButton.swift index 8e0b0d07..7fdfd19d 100644 --- a/Sources/UberAuth/Button/LoginButton.swift +++ b/Sources/UberAuth/Button/LoginButton.swift @@ -58,22 +58,32 @@ public final class LoginButton: UberButton { // MARK: Private Properties private var buttonState: State { - tokenManager.getToken() != nil ? .loggedIn : .loggedOut + tokenManager.getToken( + identifier: Constants.tokenIdentifier + ) != nil ? .loggedIn : .loggedOut } - private let tokenManager = TokenManager() + private let tokenManager: TokenManaging // MARK: Initializers public override init(frame: CGRect) { + self.tokenManager = TokenManager() super.init(frame: frame) configure() } public required init?(coder: NSCoder) { + self.tokenManager = TokenManager() super.init(coder: coder) configure() } + + public init(tokenManager: TokenManaging = TokenManager()) { + self.tokenManager = tokenManager + super.init(frame: .zero) + configure() + } // MARK: UberButton @@ -120,10 +130,12 @@ public final class LoginButton: UberButton { private func logout() { // TODO: Implement UberAuth.logout() - tokenManager.deleteToken() + tokenManager.deleteToken(identifier: Constants.tokenIdentifier) update() } + // MARK: State + enum State { case loggedIn case loggedOut @@ -147,5 +159,11 @@ public final class LoginButton: UberButton { } } } + + // MARK: Constants + + private enum Constants { + static let tokenIdentifier: String = TokenManager.defaultAccessTokenIdentifier + } } diff --git a/Sources/UberAuth/Token/TokenRequest.swift b/Sources/UberAuth/Token/TokenRequest.swift index f1a1eb66..0c2df613 100644 --- a/Sources/UberAuth/Token/TokenRequest.swift +++ b/Sources/UberAuth/Token/TokenRequest.swift @@ -4,7 +4,7 @@ import Foundation - +import UberCore /// /// Defines a network request conforming to the OAuth 2.0 standard access token request @@ -40,7 +40,7 @@ struct TokenRequest: NetworkRequest { .post } - typealias Response = Token + typealias Response = AccessToken var parameters: [String : String]? { [ @@ -52,50 +52,3 @@ struct TokenRequest: NetworkRequest { ] } } - -/// -/// The Access Token response for the authorization code grant flow as -/// defined by the OAuth 2.0 standard. -/// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 -/// -struct Token: Codable { - - let accessToken: String - let tokenType: String - let expiresIn: Int? - let refreshToken: String? - let scope: [String] - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case tokenType = "token_type" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case scope - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.accessToken = try container.decode(String.self, forKey: .accessToken) - self.tokenType = try container.decode(String.self, forKey: .tokenType) - self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn) - self.refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) - - let scopeString = try container.decodeIfPresent(String.self, forKey: .scope) - self.scope = (scopeString ?? "") - .split(separator: " ") - .map(String.init) - } - - init(accessToken: String, - tokenType: String, - expiresIn: Int? = nil, - refreshToken: String? = nil, - scope: [String] = []) { - self.accessToken = accessToken - self.tokenType = tokenType - self.expiresIn = expiresIn - self.refreshToken = refreshToken - self.scope = scope - } -} diff --git a/Sources/UberCore/Authentication/Tokens/AccessToken.swift b/Sources/UberCore/Authentication/Tokens/AccessToken.swift index e7d90577..2c031b7f 100644 --- a/Sources/UberCore/Authentication/Tokens/AccessToken.swift +++ b/Sources/UberCore/Authentication/Tokens/AccessToken.swift @@ -25,9 +25,14 @@ import Foundation +/// +/// The Access Token response for the authorization code grant flow as +/// defined by the OAuth 2.0 standard. +/// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 +/// public struct AccessToken: Codable, Equatable { - public let accessToken: String? + public let tokenString: String? public let refreshToken: String? @@ -39,24 +44,53 @@ public struct AccessToken: Codable, Equatable { // MARK: Initializers - public init(accessToken: String? = nil, + public init(tokenString: String? = nil, refreshToken: String? = nil, tokenType: String? = nil, expiresIn: Int? = nil, scope: [String]? = nil) { - self.accessToken = accessToken + self.tokenString = tokenString self.refreshToken = refreshToken self.tokenType = tokenType self.expiresIn = expiresIn self.scope = scope } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let tokenString = try container.decode(String.self, forKey: .tokenString) + let tokenType = try container.decode(String.self, forKey: .tokenType) + let expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn) + let refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) + + let scopeString = try container.decodeIfPresent(String.self, forKey: .scope) + let scope = (scopeString ?? "") + .split(separator: " ") + .map(String.init) + + self = AccessToken( + tokenString: tokenString, + refreshToken: refreshToken, + tokenType: tokenType, + expiresIn: expiresIn, + scope: scope + ) + } + + enum CodingKeys: String, CodingKey { + case tokenString = "access_token" + case tokenType = "token_type" + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + case scope + } } extension AccessToken: CustomStringConvertible { public var description: String { return """ - Access Token: \(accessToken ?? "nil") + Token String: \(tokenString ?? "nil") Refresh Token: \(refreshToken ?? "nil") Token Type: \(tokenType ?? "nil") Expires In: \(expiresIn ?? -1) diff --git a/Sources/UberCore/Authentication/Tokens/KeychainUtility.swift b/Sources/UberCore/Authentication/Tokens/KeychainUtility.swift index 3c2b104e..f43d8967 100644 --- a/Sources/UberCore/Authentication/Tokens/KeychainUtility.swift +++ b/Sources/UberCore/Authentication/Tokens/KeychainUtility.swift @@ -34,14 +34,14 @@ public protocol KeychainUtilityProtocol { /// - value: The object to save. Must conform to the Codable protocol. /// - key: A string value used to identify the saved object /// - Returns: A boolean indicating whether or not the save operation was successful - func save(_ value: V, for key: String) -> Bool + func save(_ value: V, for key: String) -> Bool /// Retrieves an object from the on device keychain using the supplied `key` /// /// - Parameters: /// - key: The identifier string used when saving the object /// - Returns: If found, an optional type conforming to the Codable protocol - func get(key: String) -> V? + func get(key: String) -> V? /// Removes the object from the on device keychain corresponding to the supplied `key` /// @@ -76,7 +76,7 @@ public final class KeychainUtility: KeychainUtilityProtocol { /// - value: The object to save. Must conform to the Codable protocol. /// - key: A string value used to identify the saved object /// - Returns: A boolean indicating whether or not the save operation was successful - public func save(_ value: V, for key: String) -> Bool { + public func save(_ value: V, for key: String) -> Bool { guard let data = try? encoder.encode(value) else { return false } @@ -108,7 +108,7 @@ public final class KeychainUtility: KeychainUtilityProtocol { /// - Parameters: /// - key: The identifier string used when saving the object /// - Returns: If found, an optional type conforming to the Codable protocol - public func get(key: String) -> V? { + public func get(key: String) -> V? { var attributes = attributes(for: key) attributes[Attribute.matchLimit] = kSecMatchLimitOne diff --git a/Sources/UberCore/Authentication/Tokens/TokenManager.swift b/Sources/UberCore/Authentication/Tokens/TokenManager.swift index 438ccd60..1ae1aff6 100644 --- a/Sources/UberCore/Authentication/Tokens/TokenManager.swift +++ b/Sources/UberCore/Authentication/Tokens/TokenManager.swift @@ -79,7 +79,6 @@ public final class TokenManager: TokenManaging { /// /// - Parameter identifier: The identifier string used when saving the Access Token /// - Returns: An optional Access Token if found - @discardableResult public func getToken(identifier: String = TokenManager.defaultAccessTokenIdentifier) -> AccessToken? { keychainUtility.get(key: identifier) } diff --git a/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift index 27fe1352..4058094e 100644 --- a/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift +++ b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift @@ -207,7 +207,7 @@ public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { public private(set) var saveCallCount = 0 public var saveHandler: ((Any, String) -> (Bool))? - public func save(_ value: V, for key: String) -> Bool { + public func save(_ value: V, for key: String) -> Bool { saveCallCount += 1 if let saveHandler = saveHandler { return saveHandler(value, key) @@ -217,7 +217,7 @@ public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { public private(set) var getCallCount = 0 public var getHandler: ((String) -> (Any?))? - public func get(key: String) -> V? { + public func get(key: String) -> V? { getCallCount += 1 if let getHandler = getHandler { return getHandler(key) as? V diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift index a84e380c..fc848bdb 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift @@ -431,8 +431,8 @@ final class AuthorizationCodeAuthProviderTests: XCTestCase { func test_nativeAuth_tokenExchange() { - let token = Token( - accessToken: "123", + let token = AccessToken( + tokenString: "123", tokenType: "test_token" ) @@ -479,9 +479,8 @@ final class AuthorizationCodeAuthProviderTests: XCTestCase { client, Client( accessToken: AccessToken( - accessToken: "123", - tokenType: "test_token", - scope: [] + tokenString: "123", + tokenType: "test_token" ) ) ) diff --git a/examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift b/examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift index d54f8e43..2453e09c 100644 --- a/examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift +++ b/examples/UberSDK/UberSDKTests/UberCore/KeychainUtilityTests.swift @@ -1,6 +1,6 @@ // // KeychainUtility.swift -// UberAuth +// UberCore // // Copyright © 2024 Uber Technologies, Inc. All rights reserved. // @@ -28,14 +28,26 @@ import XCTest final class KeychainUtilityTests: XCTestCase { + private let keychainUtility = KeychainUtility() + + override func setUp() { + super.setUp() + + _ = keychainUtility.delete(key: "test_object") + } + + override func tearDown() { + super.tearDown() + + _ = keychainUtility.delete(key: "test_object") + } + func test_save() { let testObject = TestObject( name: "test", value: 5 ) - - let keychainUtility = KeychainUtility() - + let saved = keychainUtility.save(testObject, for: "test_object") XCTAssertTrue(saved) } @@ -46,8 +58,6 @@ final class KeychainUtilityTests: XCTestCase { value: 5 ) - let keychainUtility = KeychainUtility() - _ = keychainUtility.save(testObject, for: "test_object") let retrievedObject: TestObject? = keychainUtility.get(key: "test_object") @@ -60,8 +70,6 @@ final class KeychainUtilityTests: XCTestCase { value: 5 ) - let keychainUtility = KeychainUtility() - _ = keychainUtility.save(testObject, for: "test_object") let deleted = keychainUtility.delete(key: "test_object") XCTAssertTrue(deleted) diff --git a/examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift b/examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift index 5e6bebc0..ca9cdb8b 100644 --- a/examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift +++ b/examples/UberSDK/UberSDKTests/UberCore/TokenManagerTests.swift @@ -1,6 +1,6 @@ // -// KeychainUtility.swift -// UberAuth +// TokenManagerTests.swift +// UberCore // // Copyright © 2024 Uber Technologies, Inc. All rights reserved. // @@ -38,7 +38,7 @@ final class TokenManagerTests: XCTestCase { } let accessToken = AccessToken( - accessToken: "test_token_string" + tokenString: "test_token_string" ) let tokenManager = TokenManager( @@ -53,7 +53,7 @@ final class TokenManagerTests: XCTestCase { } func test_saveToken() { - let accessToken = AccessToken(accessToken: "test_token_string") + let accessToken = AccessToken(tokenString: "test_token_string") let tokenManager = TokenManager() let result = tokenManager.saveToken(accessToken) @@ -63,7 +63,7 @@ final class TokenManagerTests: XCTestCase { func test_getToken_triggersKeychainUtilityGet() { let accessToken = AccessToken( - accessToken: "test_token_string" + tokenString: "test_token_string" ) keychainUtility.getHandler = { identifier -> AccessToken? in @@ -84,8 +84,22 @@ final class TokenManagerTests: XCTestCase { } func test_getToken() { - let accessToken = AccessToken(accessToken: "test_token_string") - let tokenManager = TokenManager() + + var savedToken: AccessToken? + keychainUtility.saveHandler = { value, _ in + savedToken = value as? AccessToken + return true + } + + keychainUtility.getHandler = { key in + XCTAssertEqual(key, TokenManager.defaultAccessTokenIdentifier) + return savedToken + } + + let accessToken = AccessToken(tokenString: "test_token_string") + let tokenManager = TokenManager( + keychainUtility: keychainUtility + ) tokenManager.saveToken(accessToken) let token = tokenManager.getToken() @@ -111,7 +125,7 @@ final class TokenManagerTests: XCTestCase { } func test_deleteToken() { - let accessToken = AccessToken(accessToken: "test_token_string") + let accessToken = AccessToken(tokenString: "test_token_string") let tokenManager = TokenManager() tokenManager.saveToken(accessToken) @@ -152,7 +166,7 @@ final class TokenManagerTests: XCTestCase { XCTAssertEqual(cookieStorage.cookies!.count, 2) XCTAssertEqual(cookieStorage.cookies(for: usUrl)!.count, 2) - let accessToken = AccessToken(accessToken: "test_token_string") + let accessToken = AccessToken(tokenString: "test_token_string") let tokenManager = TokenManager() tokenManager.saveToken(accessToken)