From b8b64f23a093e96e348f33a37f605a1a82661810 Mon Sep 17 00:00:00 2001 From: mohssen Date: Tue, 18 Apr 2023 10:23:46 -0700 Subject: [PATCH 1/2] Implement prefill Implement profile sharing --- .../AuthenticationURLUtility.swift | 6 +- .../AuthenticationProvider.swift | 12 +- .../AuthorizationCodeGrantAuthenticator.swift | 8 +- .../Authenticators/BaseAuthenticator.swift | 6 +- .../EatsAuthenticationDeeplink.swift | 4 +- .../EatsNativeAuthenticator.swift | 6 +- .../ImplicitGrantAuthenticator.swift | 7 +- .../RidesAuthenticationDeeplink.swift | 4 +- .../RidesNativeAuthenticator.swift | 6 +- .../UberCore/Authentication/LoginButton.swift | 19 ++- .../Authentication/LoginManager.swift | 73 +++++++++- .../LoginManagingProtocol.swift | 3 +- .../Authentication/OAuthEndpoint.swift | 79 +++++++++-- source/UberCore/Authentication/Par.swift | 52 +++++++ .../Authentication/PrefillValue.swift | 33 +++++ source/UberCore/Networking/APIEndpoint.swift | 5 + source/UberCore/Networking/Request.swift | 10 +- .../AuthenticationURLUtilityTests.swift | 4 +- source/UberCoreTests/LoginButtonTests.swift | 36 +++++ source/UberCoreTests/LoginManagerTests.swift | 127 +++++++++++++++++- source/UberCoreTests/OauthEndpointTests.swift | 66 ++++++++- .../UberCoreTests/RefreshEndpointTests.swift | 4 +- source/UberCoreTests/UberMocks.swift | 17 ++- source/UberRides.xcodeproj/project.pbxproj | 36 +++-- 24 files changed, 557 insertions(+), 66 deletions(-) create mode 100644 source/UberCore/Authentication/Par.swift create mode 100644 source/UberCore/Authentication/PrefillValue.swift diff --git a/source/UberCore/Authentication/AuthenticationURLUtility.swift b/source/UberCore/Authentication/AuthenticationURLUtility.swift index 739d7dc8..4cdde907 100644 --- a/source/UberCore/Authentication/AuthenticationURLUtility.swift +++ b/source/UberCore/Authentication/AuthenticationURLUtility.swift @@ -32,10 +32,11 @@ class AuthenticationURLUtility { static let scopesKey = "scope" static let sdkKey = "sdk" static let sdkVersionKey = "sdk_version" + static let requestUriKey = "request_uri" static let sdkValue = "ios" - static func buildQueryParameters(_ scopes: [UberScope]) -> [URLQueryItem] { + static func buildQueryParameters(scopes: [UberScope], requestUri: String?) -> [URLQueryItem] { var queryItems = [URLQueryItem]() queryItems.append(URLQueryItem(name: appNameKey, value: Configuration.shared.appDisplayName)) @@ -44,6 +45,9 @@ class AuthenticationURLUtility { queryItems.append(URLQueryItem(name: scopesKey, value: scopes.toUberScopeString())) queryItems.append(URLQueryItem(name: sdkKey, value: sdkValue)) queryItems.append(URLQueryItem(name: sdkVersionKey, value: Configuration.shared.sdkVersion)) + if let requestUri { + queryItems.append(URLQueryItem(name: requestUriKey, value: requestUri)) + } return queryItems } diff --git a/source/UberCore/Authentication/Authenticators/AuthenticationProvider.swift b/source/UberCore/Authentication/Authenticators/AuthenticationProvider.swift index f9bd0e1f..2b4eb3ed 100644 --- a/source/UberCore/Authentication/Authenticators/AuthenticationProvider.swift +++ b/source/UberCore/Authentication/Authenticators/AuthenticationProvider.swift @@ -30,6 +30,7 @@ class AuthenticationProvider { let productFlowPriority: [UberAuthenticationProductFlow] let scopes: [UberScope] + let requestUri: String? /// Returns an AuthenticationProvider. /// @@ -38,8 +39,9 @@ class AuthenticationProvider { /// - productFlowPriority: The product flows against which to authenticate, in the order of which Uber products you'd like to use to authenticate the user. /// /// For example, you may want to SSO with the UberEats app, but if the app does not exist on the user's device, then try to authenticate with the Uber Rides app instead. In this example you'd call this parameter with [ eats, rides ]. - init(scopes: [UberScope], productFlowPriority: [UberAuthenticationProductFlow]) { + init(scopes: [UberScope], productFlowPriority: [UberAuthenticationProductFlow], requestUri: String?) { self.scopes = scopes + self.requestUri = requestUri self.productFlowPriority = productFlowPriority } @@ -54,16 +56,16 @@ class AuthenticationProvider { switch loginType { case .authorizationCode: // Rides and Eats temporarily share the same authorization code flow - return AuthorizationCodeGrantAuthenticator(scopes: scopes) + return AuthorizationCodeGrantAuthenticator(scopes: scopes, requestUri: requestUri) case .implicit: // Rides and Eats temporarily share the same implicit grant code flow - return ImplicitGrantAuthenticator(scopes: scopes) + return ImplicitGrantAuthenticator(scopes: scopes, requestUri: requestUri) case .native: switch authProduct.uberProductType { case .rides: - return RidesNativeAuthenticator(scopes: scopes) + return RidesNativeAuthenticator(scopes: scopes, requestUri: requestUri) case .eats: - return EatsNativeAuthenticator(scopes: scopes) + return EatsNativeAuthenticator(scopes: scopes, requestUri: requestUri) } } } diff --git a/source/UberCore/Authentication/Authenticators/AuthorizationCodeGrantAuthenticator.swift b/source/UberCore/Authentication/Authenticators/AuthorizationCodeGrantAuthenticator.swift index 04a061e4..43a0dd8b 100644 --- a/source/UberCore/Authentication/Authenticators/AuthorizationCodeGrantAuthenticator.swift +++ b/source/UberCore/Authentication/Authenticators/AuthorizationCodeGrantAuthenticator.swift @@ -28,6 +28,12 @@ import UIKit @objc public var state: String? @objc override var authorizationURL: URL { - return OAuth.authorizationCodeLogin(clientID: Configuration.shared.clientID, redirect: Configuration.shared.getCallbackURI(for: .authorizationCode), scopes: scopes, state: state).url + return OAuth.authorizationCodeLogin( + clientID: Configuration.shared.clientID, + redirect: Configuration.shared.getCallbackURI(for: .authorizationCode), + scopes: scopes, + state: state, + requestUri: requestUri + ).url } } diff --git a/source/UberCore/Authentication/Authenticators/BaseAuthenticator.swift b/source/UberCore/Authentication/Authenticators/BaseAuthenticator.swift index 60c664ff..8a8123b9 100644 --- a/source/UberCore/Authentication/Authenticators/BaseAuthenticator.swift +++ b/source/UberCore/Authentication/Authenticators/BaseAuthenticator.swift @@ -29,8 +29,12 @@ import UIKit /// Scopes to request during login @objc public var scopes: [UberScope] - @objc public init(scopes: [UberScope]) { + @objc public var requestUri: String? + + @objc public init(scopes: [UberScope], + requestUri: String? = nil) { self.scopes = scopes + self.requestUri = requestUri super.init() } diff --git a/source/UberCore/Authentication/Authenticators/EatsAuthenticationDeeplink.swift b/source/UberCore/Authentication/Authenticators/EatsAuthenticationDeeplink.swift index 1675aecc..49605213 100644 --- a/source/UberCore/Authentication/Authenticators/EatsAuthenticationDeeplink.swift +++ b/source/UberCore/Authentication/Authenticators/EatsAuthenticationDeeplink.swift @@ -36,8 +36,8 @@ import Foundation - returns: An initialized AuthenticationDeeplink */ - @objc public init(scopes: [UberScope]) { - let queryItems = AuthenticationURLUtility.buildQueryParameters(scopes) + @objc public init(scopes: [UberScope], requestUri: String?) { + let queryItems = AuthenticationURLUtility.buildQueryParameters(scopes: scopes, requestUri: requestUri) let scheme = "eatsauth" let domain = "connect" diff --git a/source/UberCore/Authentication/Authenticators/EatsNativeAuthenticator.swift b/source/UberCore/Authentication/Authenticators/EatsNativeAuthenticator.swift index d808e35e..d5b47448 100644 --- a/source/UberCore/Authentication/Authenticators/EatsNativeAuthenticator.swift +++ b/source/UberCore/Authentication/Authenticators/EatsNativeAuthenticator.swift @@ -41,8 +41,8 @@ import Foundation - returns: true if a redirect was handled, false otherwise. */ - @objc public override init(scopes: [UberScope]) { - deeplink = EatsAuthenticationDeeplink(scopes: scopes) - super.init(scopes: scopes) + @objc public override init(scopes: [UberScope], requestUri: String?) { + deeplink = EatsAuthenticationDeeplink(scopes: scopes, requestUri: requestUri) + super.init(scopes: scopes, requestUri: requestUri) } } diff --git a/source/UberCore/Authentication/Authenticators/ImplicitGrantAuthenticator.swift b/source/UberCore/Authentication/Authenticators/ImplicitGrantAuthenticator.swift index ed306204..b3a3ff07 100644 --- a/source/UberCore/Authentication/Authenticators/ImplicitGrantAuthenticator.swift +++ b/source/UberCore/Authentication/Authenticators/ImplicitGrantAuthenticator.swift @@ -27,6 +27,11 @@ */ @objc(UBSDKImplicitGrantAuthenticator) public class ImplicitGrantAuthenticator: BaseAuthenticator { @objc override var authorizationURL: URL { - return OAuth.implicitLogin(clientID: Configuration.shared.clientID, scopes: self.scopes, redirect: Configuration.shared.getCallbackURI(for: .implicit)).url + return OAuth.implicitLogin( + clientID: Configuration.shared.clientID, + scopes: scopes, + redirect: Configuration.shared.getCallbackURI(for: .implicit), + requestUri: requestUri + ).url } } diff --git a/source/UberCore/Authentication/Authenticators/RidesAuthenticationDeeplink.swift b/source/UberCore/Authentication/Authenticators/RidesAuthenticationDeeplink.swift index 9891ce07..3f2ea771 100644 --- a/source/UberCore/Authentication/Authenticators/RidesAuthenticationDeeplink.swift +++ b/source/UberCore/Authentication/Authenticators/RidesAuthenticationDeeplink.swift @@ -36,8 +36,8 @@ import Foundation - returns: An initialized AuthenticationDeeplink */ - @objc public init(scopes: [UberScope]) { - let queryItems = AuthenticationURLUtility.buildQueryParameters(scopes) + @objc public init(scopes: [UberScope], requestUri: String? = nil) { + let queryItems = AuthenticationURLUtility.buildQueryParameters(scopes: scopes, requestUri: requestUri) let scheme = "uberauth" let domain = "connect" diff --git a/source/UberCore/Authentication/Authenticators/RidesNativeAuthenticator.swift b/source/UberCore/Authentication/Authenticators/RidesNativeAuthenticator.swift index ec0f5624..6fafd3c8 100644 --- a/source/UberCore/Authentication/Authenticators/RidesNativeAuthenticator.swift +++ b/source/UberCore/Authentication/Authenticators/RidesNativeAuthenticator.swift @@ -41,8 +41,8 @@ import Foundation - returns: true if a redirect was handled, false otherwise. */ - @objc public override init(scopes: [UberScope]) { - deeplink = RidesAuthenticationDeeplink(scopes: scopes) - super.init(scopes: scopes) + @objc public override init(scopes: [UberScope], requestUri: String?) { + deeplink = RidesAuthenticationDeeplink(scopes: scopes, requestUri: requestUri) + super.init(scopes: scopes, requestUri: requestUri) } } diff --git a/source/UberCore/Authentication/LoginButton.swift b/source/UberCore/Authentication/LoginButton.swift index 8f08b635..70d10662 100644 --- a/source/UberCore/Authentication/LoginButton.swift +++ b/source/UberCore/Authentication/LoginButton.swift @@ -43,7 +43,7 @@ import UIKit @objc func loginButton(_ button: LoginButton, didLogoutWithSuccess success: Bool) /** - THe Login Button completed a login + The Login Button completed a login - parameter button: The LoginButton involved - parameter accessToken: The access token that @@ -52,6 +52,14 @@ import UIKit @objc func loginButton(_ button: LoginButton, didCompleteLoginWithToken accessToken: AccessToken?, error: NSError?) } +/** + * Protocol to provide content for login + */ +@objc(UBSDKLoginButtonDataSource) public protocol LoginButtonDataSource { + + @objc func prefillValues(_ button: LoginButton) -> Prefill? +} + /// Button to handle logging in to Uber @objc(UBSDKLoginButton) public class LoginButton: UberButton { @@ -62,6 +70,8 @@ import UIKit /// The LoginButtonDelegate for this button @objc public weak var delegate: LoginButtonDelegate? + @objc public weak var dataSource: LoginButtonDataSource? + /// The LoginManager to use for log in @objc public var loginManager: LoginManager { didSet { @@ -206,7 +216,12 @@ import UIKit delegate?.loginButton(self, didLogoutWithSuccess: success) refreshContent() case .signedOut: - loginManager.login(requestedScopes: scopes, presentingViewController: presentingViewController, completion: loginCompletion) + loginManager.login( + requestedScopes: scopes, + presentingViewController: presentingViewController, + prefillValues: dataSource?.prefillValues(self), + completion: loginCompletion + ) } } diff --git a/source/UberCore/Authentication/LoginManager.swift b/source/UberCore/Authentication/LoginManager.swift index 36cb2517..234a9106 100644 --- a/source/UberCore/Authentication/LoginManager.swift +++ b/source/UberCore/Authentication/LoginManager.swift @@ -36,6 +36,7 @@ import SafariServices var loggingIn: Bool = false var willEnterForegroundCalled: Bool = false private var postCompletionHandler: AuthenticationCompletionHandler? + private let urlSession = URLSession(configuration: .default) /** Create instance of login manager to authenticate user and retreive access token. @@ -187,17 +188,44 @@ import SafariServices - parameter scopes: scopes being requested. - parameter presentingViewController: The presenting view controller present the login view controller over. + - parameter prefillValues: Optional values to pre-populate the signin form with. - parameter completion: The LoginManagerRequestTokenHandler completion handler for login success/failure. */ - @objc public func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController? = nil, completion: AuthenticationCompletionHandler? = nil) { + @objc public func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController? = nil, prefillValues: Prefill? = nil, completion: AuthenticationCompletionHandler? = nil) { self.postCompletionHandler = completion UberAppDelegate.shared.loginManager = self - let authProvider = AuthenticationProvider(scopes: scopes, productFlowPriority: productFlowPriority) - loggingIn = true willEnterForegroundCalled = false - executeLogin(presentingViewController: presentingViewController, authenticationProvider: authProvider) + + let executeLogin: (String?) -> Void = { [weak self] requestUri in + guard let self = self else { + return + } + let authProvider = AuthenticationProvider(scopes: scopes, productFlowPriority: self.productFlowPriority, requestUri: requestUri) + self.executeLogin(presentingViewController: presentingViewController, authenticationProvider: authProvider) + } + + let responseType: OAuth.ResponseType? = { + switch loginType { + case .authorizationCode: + return .code + case .implicit: + return .token + case .native: + return nil + } + }() + + if let prefillValues = prefillValues, + let responseType = responseType { + executeParRequest(prefillValues: prefillValues, + responseType: responseType) { requestUri in + executeLogin(requestUri) + } + } else { + executeLogin(nil) + } } /** @@ -279,6 +307,42 @@ import SafariServices // Mark: Private Interface + private func executeParRequest(prefillValues: Prefill, + responseType: OAuth.ResponseType, + _ completion: @escaping (String?) -> Void) { + + let loginHint = prefillValues.dictValue + guard !loginHint.isEmpty else { + completion(nil) + return + } + + let request = Request( + session: urlSession, + endpoint: OAuth.par( + clientID: Configuration.shared.clientID, + loginHint: loginHint, + responseType: responseType + ) + ) + + request?.prepare() + request?.execute { response in + let requestUri: String? = { + guard let data = response.data, + response.error == nil, + let par = try? JSONDecoder.uberDecoder.decode(Par.self, from: data) else { + return nil + } + return par.requestUri + }() + + DispatchQueue.main.async { + completion(requestUri) + } + } + } + private func executeLogin(presentingViewController: UIViewController?, authenticationProvider: AuthenticationProvider) { if let authenticator = authenticationProvider.authenticators(for: loginType).first, authenticator.authorizationURL.scheme == "https" { executeWebLogin(presentingViewController: presentingViewController, authenticator: authenticator) @@ -413,4 +477,5 @@ import SafariServices postCompletionHandler?(accessToken, error) } + } diff --git a/source/UberCore/Authentication/LoginManagingProtocol.swift b/source/UberCore/Authentication/LoginManagingProtocol.swift index 3de276e7..8f58f998 100644 --- a/source/UberCore/Authentication/LoginManagingProtocol.swift +++ b/source/UberCore/Authentication/LoginManagingProtocol.swift @@ -41,9 +41,10 @@ - parameter scopes: scopes being requested. - parameter presentingViewController: The presenting view controller present the login view controller over. + - parameter prefillValues: Optional values to pre-populate the signin form with. - parameter completion: The LoginManagerRequestTokenHandler completion handler for login success/failure. */ - @objc func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController?, completion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)?) + @objc func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController?, prefillValues: Prefill?, completion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)?) /** Called via the RidesAppDelegate when the application is opened via a URL. Responsible diff --git a/source/UberCore/Authentication/OAuthEndpoint.swift b/source/UberCore/Authentication/OAuthEndpoint.swift index a7c7d083..587f77ab 100644 --- a/source/UberCore/Authentication/OAuthEndpoint.swift +++ b/source/UberCore/Authentication/OAuthEndpoint.swift @@ -30,23 +30,24 @@ - Refresh: Used to refresh an access token that has been aquired via SSO */ public enum OAuth: APIEndpoint { - case implicitLogin(clientID: String, scopes: [UberScope], redirect: URL) - case authorizationCodeLogin(clientID: String, redirect: URL, scopes: [UberScope], state: String?) + case implicitLogin(clientID: String, scopes: [UberScope], redirect: URL, requestUri: String? = nil) + case authorizationCodeLogin(clientID: String, redirect: URL, scopes: [UberScope], state: String?, requestUri: String? = nil) case refresh(clientID: String, refreshToken: String) + case par(clientID: String, loginHint: [String: String], responseType: ResponseType) public var method: UberHTTPMethod { switch self { - case .implicitLogin: - fallthrough - case .authorizationCodeLogin: + case .implicitLogin, + .authorizationCodeLogin: return .get - case .refresh: + case .refresh, + .par: return .post } } public var host: String { - return OAuth.regionHost + OAuth.regionHost } public var body: Data? { @@ -59,13 +60,23 @@ public enum OAuth: APIEndpoint { var components = URLComponents() components.queryItems = query return components.query?.data(using: String.Encoding.utf8) + case .par(let clientID, let loginHint, let responseType): + let loginHintString = (try? JSONSerialization.data(withJSONObject: loginHint))?.base64EncodedString() ?? "" + let query = queryBuilder( + ("client_id", clientID), + ("response_type", responseType.rawValue), + ("login_hint", loginHintString) + ) + var components = URLComponents() + components.queryItems = query + return components.query?.data(using: String.Encoding.utf8) default: return nil } } static var regionHost: String { - return "https://login.uber.com" + return "https://auth.uber.com" } public var path: String { @@ -76,27 +87,62 @@ public enum OAuth: APIEndpoint { return "/oauth/v2/authorize" case .refresh: return "/oauth/v2/mobile/token" + case .par: + return "/oauth/v2/par" } } public var query: [URLQueryItem] { switch self { - case .implicitLogin(let clientID, let scopes, let redirect): + case .implicitLogin(let clientID, let scopes, let redirect, let requestUri): var loginQuery = baseLoginQuery(clientID, redirect: redirect, scopes: scopes) - let additionalQueryItems = queryBuilder(("response_type", "token")) + let additionalQueryItems: [URLQueryItem] = [ + ("response_type", ResponseType.token.rawValue), + ("request_uri", requestUri) + ] + .compactMap { pair -> [URLQueryItem]? in + guard let value = pair.1 else { + return nil + } + return self.queryBuilder((pair.0, value)) + } + .flatMap { $0 } loginQuery.append(contentsOf: additionalQueryItems) return loginQuery - case .authorizationCodeLogin(let clientID, let redirect, let scopes, let state): + case .authorizationCodeLogin(let clientID, let redirect, let scopes, let state, let requestUri): var loginQuery = baseLoginQuery(clientID, redirect: redirect, scopes: scopes) - let additionalQueryItems = queryBuilder(("response_type", "code"), - ("state", state ?? "")) + let additionalQueryItems: [URLQueryItem] = [ + ("response_type", ResponseType.code.rawValue), + ("state", state ?? ""), + ("request_uri", requestUri) + ] + .compactMap { pair -> [URLQueryItem]? in + guard let value = pair.1 else { + return nil + } + return self.queryBuilder((pair.0, value)) + } + .flatMap { $0 } loginQuery.append(contentsOf: additionalQueryItems) return loginQuery + case .par: + return queryBuilder() case .refresh: return queryBuilder() } } + + public var contentType: String? { + switch self { + case .implicitLogin, + .authorizationCodeLogin, + .refresh: + return nil + case .par: + return "application/x-www-form-urlencoded" + } + } func baseLoginQuery(_ clientID: String, redirect: URL, scopes: [UberScope]) -> [URLQueryItem] { @@ -116,4 +162,11 @@ public enum OAuth: APIEndpoint { return "" } } + + // MARK: - ResponseType + + public enum ResponseType: String { + case code + case token + } } diff --git a/source/UberCore/Authentication/Par.swift b/source/UberCore/Authentication/Par.swift new file mode 100644 index 00000000..efc77592 --- /dev/null +++ b/source/UberCore/Authentication/Par.swift @@ -0,0 +1,52 @@ +// +// Ride.swift +// UberRides +// +// Copyright © 2016 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. + +// MARK: Par + +/** + * Contains the status of an ongoing/completed trip created using the Ride Request endpoint + */ +@objc(UBSDKPar) public class Par: NSObject, Decodable { + + /// An identifier used for profile sharing + @objc public private(set) var requestUri: String? + + /// Lifetime of the request_uri + @objc public private(set) var expiresIn: NSNumber? + + enum CodingKeys: String, CodingKey { + case requestUri = "request_uri" + case expiresIn = "expires_in" + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + requestUri = try container.decodeIfPresent(String.self, forKey: .requestUri) + if let expiration = try container.decodeIfPresent(Int64.self, forKey: .expiresIn) { + expiresIn = NSNumber(value: expiration) + } else { + expiresIn = nil + } + } +} diff --git a/source/UberCore/Authentication/PrefillValue.swift b/source/UberCore/Authentication/PrefillValue.swift new file mode 100644 index 00000000..8f759a96 --- /dev/null +++ b/source/UberCore/Authentication/PrefillValue.swift @@ -0,0 +1,33 @@ +// +// Copyright © Uber Technologies, Inc. All rights reserved. +// + + +import Foundation + +@objc public class Prefill: NSObject { + public let email: String? + public let phoneNumber: String? + public let firstName: String? + public let lastName: String? + + public init(email: String? = nil, + phoneNumber: String? = nil, + firstName: String? = nil, + lastName: String? = nil) { + self.email = email + self.phoneNumber = phoneNumber + self.firstName = firstName + self.lastName = lastName + } + + var dictValue: [String: String] { + [ + "email": email, + "phone": phoneNumber, + "first_name": firstName, + "last_name": lastName + ] + .compactMapValues { $0 } + } +} diff --git a/source/UberCore/Networking/APIEndpoint.swift b/source/UberCore/Networking/APIEndpoint.swift index b52b4684..be706ebe 100644 --- a/source/UberCore/Networking/APIEndpoint.swift +++ b/source/UberCore/Networking/APIEndpoint.swift @@ -32,6 +32,7 @@ public protocol APIEndpoint { var method: UberHTTPMethod { get } var path: String { get } var query: [URLQueryItem] { get } + var contentType: String? { get } } public extension APIEndpoint { @@ -77,6 +78,10 @@ public extension APIEndpoint { } return queryItems } + + var contentType: String? { + return nil + } } /** diff --git a/source/UberCore/Networking/Request.swift b/source/UberCore/Networking/Request.swift index 0c36b176..ce1ada8e 100644 --- a/source/UberCore/Networking/Request.swift +++ b/source/UberCore/Networking/Request.swift @@ -114,10 +114,12 @@ public class Request { } else if let token = serverToken { urlRequest.setValue("Token \(token)", forHTTPHeaderField: "Authorization") } - if let headers = endpoint.headers { - for (header,value) in headers { - urlRequest.setValue(value, forHTTPHeaderField: header) - } + var headers = endpoint.headers ?? [:] + if let contentType = endpoint.contentType { + headers["Content-Type"] = contentType + } + for (header,value) in headers { + urlRequest.setValue(value, forHTTPHeaderField: header) } } diff --git a/source/UberCoreTests/AuthenticationURLUtilityTests.swift b/source/UberCoreTests/AuthenticationURLUtilityTests.swift index 6ec47008..9f7768a4 100644 --- a/source/UberCoreTests/AuthenticationURLUtilityTests.swift +++ b/source/UberCoreTests/AuthenticationURLUtilityTests.swift @@ -61,7 +61,7 @@ class AuthenticationURLUtilityTests: XCTestCase { let expectedQueryItems = [scopeQueryItem, clientIDQueryItem, appNameQueryItem, callbackURIQueryItem, sdkQueryItem, sdkVersionQueryItem] let comparisonSet = NSSet(array: expectedQueryItems) - let testQueryItems = AuthenticationURLUtility.buildQueryParameters(scopes) + let testQueryItems = AuthenticationURLUtility.buildQueryParameters(scopes: scopes, requestUri: nil) let testComparisonSet = NSSet(array:testQueryItems) XCTAssertEqual(comparisonSet, testComparisonSet) @@ -88,7 +88,7 @@ class AuthenticationURLUtilityTests: XCTestCase { let expectedQueryItems = [scopeQueryItem, clientIDQueryItem, appNameQueryItem, callbackURIQueryItem, sdkQueryItem, sdkVersionQueryItem] let comparisonSet = NSSet(array: expectedQueryItems) - let testQueryItems = AuthenticationURLUtility.buildQueryParameters(scopes) + let testQueryItems = AuthenticationURLUtility.buildQueryParameters(scopes: scopes, requestUri: nil) let testComparisonSet = NSSet(array:testQueryItems) XCTAssertEqual(comparisonSet, testComparisonSet) diff --git a/source/UberCoreTests/LoginButtonTests.swift b/source/UberCoreTests/LoginButtonTests.swift index af374894..2f2f53d6 100644 --- a/source/UberCoreTests/LoginButtonTests.swift +++ b/source/UberCoreTests/LoginButtonTests.swift @@ -28,6 +28,7 @@ class LoginButtonTests : XCTestCase { private var keychain: KeychainWrapper! private var testToken: AccessToken! + private var testDataSource: TestDataSource! override func setUp() { super.setUp() @@ -122,4 +123,39 @@ class LoginButtonTests : XCTestCase { _ = keychain.deleteObjectForKey(identifier) } + + func test_loginButton_callsDataSourceOnTap() { + let identifier = "testIdentifier" + + let loginManager = LoginManager(accessTokenIdentifier: identifier, keychainAccessGroup: nil, loginType: .implicit) + let loginButton = LoginButton(frame: CGRect.zero, scopes: [.profile], loginManager: loginManager) + + let expectation = self.expectation(description: "Prefill handler called") + + let prefillHandler: () -> Prefill? = { + expectation.fulfill() + return nil + } + + testDataSource = TestDataSource(prefillHandler) + loginButton.dataSource = testDataSource + loginButton.uberButtonTapped(loginButton) + + waitForExpectations(timeout: 0.2) + } + + // MARK: - TestDataSource + + fileprivate class TestDataSource: LoginButtonDataSource { + + private let prefillValueHandler: () -> Prefill? + + init(_ prefillValueHandler: @escaping () -> Prefill?) { + self.prefillValueHandler = prefillValueHandler + } + + func prefillValues(_ button: LoginButton) -> Prefill? { + prefillValueHandler() + } + } } diff --git a/source/UberCoreTests/LoginManagerTests.swift b/source/UberCoreTests/LoginManagerTests.swift index 039c05bd..bac1dc51 100644 --- a/source/UberCoreTests/LoginManagerTests.swift +++ b/source/UberCoreTests/LoginManagerTests.swift @@ -22,6 +22,8 @@ import XCTest import CoreLocation +import OHHTTPStubs +import OHHTTPStubsSwift import WebKit @testable import UberCore @@ -87,7 +89,7 @@ class LoginManagerTests: XCTestCase { completionHandler?(nil, UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .unavailable)) } - loginManagerMock.login(requestedScopes: [.profile], presentingViewController: nil, completion: loginCompletion) + loginManagerMock.login(requestedScopes: [.profile], presentingViewController: nil, prefillValues: nil, completion: loginCompletion) waitForExpectations(timeout: 0.2, handler: nil) } @@ -268,5 +270,128 @@ class LoginManagerTests: XCTestCase { XCTAssertEqual(loginManager.loginType, LoginType.implicit) } + + func testImplicitLogin_executesParRequest() { + + let expectation = expectation(description: "Implicit login PAR endpoint called") + + stub(condition: isPath("/oauth/v2/par")) { _ in + expectation.fulfill() + return HTTPStubsResponse() + } + + let loginManager = LoginManager( + loginType: .implicit, + productFlowPriority: [UberAuthenticationProductFlow(.eats)] + ) + + loginManager.login( + requestedScopes: [UberScope.profile], + presentingViewController: UIViewController(), + prefillValues: Prefill(email: "test@test.com"), + completion: nil + ) + + waitForExpectations(timeout: 0.2, handler: nil) + } + + func testAuthorizationCodeLogin_executesParRequest() { + + let expectation = expectation(description: "Authorization code login PAR endpoint called") + + stub(condition: isPath("/oauth/v2/par")) { _ in + expectation.fulfill() + return HTTPStubsResponse() + } + + let loginManager = LoginManager( + loginType: .authorizationCode, + productFlowPriority: [UberAuthenticationProductFlow(.eats)] + ) + + loginManager.login( + requestedScopes: [UberScope.profile], + presentingViewController: UIViewController(), + prefillValues: Prefill(email: "test@test.com"), + completion: nil + ) + + waitForExpectations(timeout: 0.2, handler: nil) + } + + func testLogin_withNoPrefillValues_doesNotExecutesParRequest() { + + let expectation = expectation(description: "No prefill login PAR endpoint not called") + expectation.isInverted = true + + stub(condition: isPath("/oauth/v2/par")) { _ in + expectation.fulfill() + return HTTPStubsResponse() + } + + let loginManager = LoginManager( + loginType: .authorizationCode, + productFlowPriority: [UberAuthenticationProductFlow(.eats)] + ) + + loginManager.login( + requestedScopes: [UberScope.profile], + presentingViewController: UIViewController(), + prefillValues: nil, + completion: nil + ) + + waitForExpectations(timeout: 0.2, handler: nil) + } + + func testLogin_withEmptyPrefillValues_doesNotExecutesParRequest() { + + let expectation = expectation(description: "EmptyPrefill login PAR endpoint not called") + expectation.isInverted = true + + stub(condition: isPath("/oauth/v2/par")) { _ in + expectation.fulfill() + return HTTPStubsResponse() + } + + let loginManager = LoginManager( + loginType: .authorizationCode, + productFlowPriority: [UberAuthenticationProductFlow(.eats)] + ) + + loginManager.login( + requestedScopes: [UberScope.profile], + presentingViewController: UIViewController(), + prefillValues: Prefill(), + completion: nil + ) + + waitForExpectations(timeout: 0.2, handler: nil) + } + + func testNativeLogin_doesNotExecuteParRequest() { + + let expectation = expectation(description: "Native login PAR endpoint not called") + expectation.isInverted = true + + stub(condition: isPath("/oauth/v2/par")) { _ in + expectation.fulfill() + return HTTPStubsResponse() + } + + let loginManager = LoginManager( + loginType: .native, + productFlowPriority: [UberAuthenticationProductFlow(.eats)] + ) + + loginManager.login( + requestedScopes: [UberScope.profile], + presentingViewController: UIViewController(), + prefillValues: Prefill(), + completion: nil + ) + + waitForExpectations(timeout: 0.2, handler: nil) + } } diff --git a/source/UberCoreTests/OauthEndpointTests.swift b/source/UberCoreTests/OauthEndpointTests.swift index 9128fbea..dffba3ec 100644 --- a/source/UberCoreTests/OauthEndpointTests.swift +++ b/source/UberCoreTests/OauthEndpointTests.swift @@ -45,7 +45,7 @@ class OauthEndpointTests: XCTestCase { Configuration.shared.isSandbox = true let scopes = [ UberScope.profile, UberScope.history ] - let expectedHost = "https://login.uber.com" + let expectedHost = "https://auth.uber.com" let expectedPath = "/oauth/v2/authorize" let expectedScopes = scopes.toUberScopeString() let expectedClientID = Configuration.shared.clientID @@ -70,7 +70,7 @@ class OauthEndpointTests: XCTestCase { Configuration.shared.isSandbox = false let scopes = [ UberScope.profile, UberScope.history ] - let expectedHost = "https://login.uber.com" + let expectedHost = "https://auth.uber.com" let expectedPath = "/oauth/v2/authorize" let expectedScopes = scopes.toUberScopeString() let expectedClientID = Configuration.shared.clientID @@ -93,7 +93,7 @@ class OauthEndpointTests: XCTestCase { func testLogin_forAuthorizationCodeGrant_defaultSettings() { let scopes = [ UberScope.allTrips, UberScope.history ] - let expectedHost = "https://login.uber.com" + let expectedHost = "https://auth.uber.com" let expectedPath = "/oauth/v2/authorize" let expectedScopes = scopes.toUberScopeString() let expectedClientID = Configuration.shared.clientID @@ -116,4 +116,64 @@ class OauthEndpointTests: XCTestCase { XCTAssertEqual(login.path, expectedPath) XCTAssertEqual(login.query, expectedQueryItems) } + + func testPar_forResponseTypeCode_defaultSettings() { + let expectedHost = "https://auth.uber.com" + let expectedPath = "/oauth/v2/par" + let expectedClientID = Configuration.shared.clientID + let expectedResponseType = OAuth.ResponseType.code + let expectedLoginHint: [String: String] = [ + "email": "test@test.com", + "phone": "5555555555", + "first_name": "First", + "last_name": "Last" + ] + let expectedLoginHintString = try! JSONSerialization.data(withJSONObject: expectedLoginHint).base64EncodedString() + + var components = URLComponents() + components.queryItems = [ + URLQueryItem(name: "client_id", value: expectedClientID), + URLQueryItem(name: "response_type", value: expectedResponseType.rawValue), + URLQueryItem(name: "login_hint", value: expectedLoginHintString), + ] + let expectedQueryItems = components.query! + + let login = OAuth.par(clientID: expectedClientID, loginHint: expectedLoginHint, responseType: expectedResponseType) + + let queryString = String(data: login.body!, encoding: .utf8) + + XCTAssertEqual(login.host, expectedHost) + XCTAssertEqual(login.path, expectedPath) + XCTAssertEqual(queryString, expectedQueryItems) + } + + func testPar_forResponseTypeToken_defaultSettings() { + let expectedHost = "https://auth.uber.com" + let expectedPath = "/oauth/v2/par" + let expectedClientID = Configuration.shared.clientID + let expectedResponseType = OAuth.ResponseType.token + let expectedLoginHint: [String: String] = [ + "email": "test@test.com", + "phone": "5555555555", + "first_name": "First", + "last_name": "Last" + ] + let expectedLoginHintString = try! JSONSerialization.data(withJSONObject: expectedLoginHint).base64EncodedString() + + var components = URLComponents() + components.queryItems = [ + URLQueryItem(name: "client_id", value: expectedClientID), + URLQueryItem(name: "response_type", value: expectedResponseType.rawValue), + URLQueryItem(name: "login_hint", value: expectedLoginHintString), + ] + let expectedQueryItems = components.query! + + let login = OAuth.par(clientID: expectedClientID, loginHint: expectedLoginHint, responseType: expectedResponseType) + + let queryString = String(data: login.body!, encoding: .utf8) + + XCTAssertEqual(login.host, expectedHost) + XCTAssertEqual(login.path, expectedPath) + XCTAssertEqual(queryString, expectedQueryItems) + } } diff --git a/source/UberCoreTests/RefreshEndpointTests.swift b/source/UberCoreTests/RefreshEndpointTests.swift index a41a7efa..789ed55e 100644 --- a/source/UberCoreTests/RefreshEndpointTests.swift +++ b/source/UberCoreTests/RefreshEndpointTests.swift @@ -52,7 +52,7 @@ class RefreshEndpointTests: XCTestCase { Test 200 success response */ func test200Response() { - stub(condition: isHost("login.uber.com")) { _ in + stub(condition: isHost("auth.uber.com")) { _ in return HTTPStubsResponse(fileAtPath:OHPathForFile("refresh.json", type(of: self))!, statusCode:200, headers:self.headers) } let refreshToken = "ThisIsRefresh" @@ -104,7 +104,7 @@ class RefreshEndpointTests: XCTestCase { func test400Error() { let error = "invalid_refresh_token" - stub(condition: isHost("login.uber.com")) { _ in + stub(condition: isHost("auth.uber.com")) { _ in let json = ["error": error] return HTTPStubsResponse(jsonObject: json, statusCode: 400, headers: self.headers) } diff --git a/source/UberCoreTests/UberMocks.swift b/source/UberCoreTests/UberMocks.swift index ddef0ef1..01fd7270 100644 --- a/source/UberCoreTests/UberMocks.swift +++ b/source/UberCoreTests/UberMocks.swift @@ -26,7 +26,10 @@ class LoginManagerPartialMock: LoginManager { var executeLoginClosure: ((AuthenticationCompletionHandler?) -> ())? - @objc public override func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController? = nil, completion: AuthenticationCompletionHandler? = nil) { + @objc public override func login(requestedScopes scopes: [UberScope], + presentingViewController: UIViewController?, + prefillValues: Prefill?, + completion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)?) { executeLoginClosure?(completion) } } @@ -45,11 +48,19 @@ class LoginManagerPartialMock: LoginManager { super.init() } - func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController?, completion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)?) { + func login(requestedScopes scopes: [UberScope], + presentingViewController: UIViewController?, + prefillValues: Prefill?, + completion: ((_ accessToken: AccessToken?, _ error: NSError?) -> Void)?) { if let closure = loginClosure { closure(scopes, presentingViewController, completion) } else if let manager = backingManager { - manager.login(requestedScopes: scopes, presentingViewController: presentingViewController, completion: completion) + manager.login( + requestedScopes: scopes, + presentingViewController: presentingViewController, + prefillValues: nil, + completion: completion + ) } } diff --git a/source/UberRides.xcodeproj/project.pbxproj b/source/UberRides.xcodeproj/project.pbxproj index 95e254de..9e028bca 100644 --- a/source/UberRides.xcodeproj/project.pbxproj +++ b/source/UberRides.xcodeproj/project.pbxproj @@ -27,6 +27,11 @@ B261EBAA29E4A9A3007CDCC3 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B261EBA929E4A9A3007CDCC3 /* OHHTTPStubsSwift */; }; B261EBAC29E4A9A9007CDCC3 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = B261EBAB29E4A9A9007CDCC3 /* OHHTTPStubs */; }; B261EBAE29E4A9A9007CDCC3 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B261EBAD29E4A9A9007CDCC3 /* OHHTTPStubsSwift */; }; + B289FC9F29D5FB560071FFF4 /* UberCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFFCA8AC1F96A4800032E6BA /* UberCore.framework */; }; + B289FCA029D5FB7C0071FFF4 /* UberRides.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC0404751BFACD1D00AC1501 /* UberRides.framework */; }; + B289FCA229D6123B0071FFF4 /* PrefillValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B289FCA129D6123B0071FFF4 /* PrefillValue.swift */; }; + B291DED929EF09D1005DD810 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B291DED729EF09D1005DD810 /* Main.storyboard */; }; + B2D31CA429E88C4B001A9891 /* Par.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D31CA329E88C4B001A9891 /* Par.swift */; }; D81A8B161C658A2800339C13 /* TimeEstimate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A8B151C658A2800339C13 /* TimeEstimate.swift */; }; D81A8B181C658A4F00339C13 /* getTimeEstimates.json in Resources */ = {isa = PBXBuildFile; fileRef = D81A8B171C658A4F00339C13 /* getTimeEstimates.json */; }; D81A8B1A1C69186B00339C13 /* RequestLayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A8B191C69186B00339C13 /* RequestLayerTests.swift */; }; @@ -99,7 +104,6 @@ DCD8060C1CFE50F300EF6EB1 /* requestEstimateNoCars.json in Resources */ = {isa = PBXBuildFile; fileRef = DCD8060B1CFE50F300EF6EB1 /* requestEstimateNoCars.json */; }; DCDD153E1D99A7F20053BC8F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDD153D1D99A7F20053BC8F /* AppDelegate.swift */; }; DCDD15401D99A7F20053BC8F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDD153F1D99A7F20053BC8F /* ViewController.swift */; }; - DCDD15431D99A7F20053BC8F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCDD15411D99A7F20053BC8F /* Main.storyboard */; }; DCDD15451D99A7F20053BC8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCDD15441D99A7F20053BC8F /* Assets.xcassets */; }; DCDD15481D99A7F20053BC8F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCDD15461D99A7F20053BC8F /* LaunchScreen.storyboard */; }; DF021B3E1F996158004A7893 /* KeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CC80FA1C7D3AA300385AD5 /* KeychainWrapper.swift */; }; @@ -215,6 +219,9 @@ AC0404931BFACDAF00AC1501 /* RequestDeeplink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestDeeplink.swift; sourceTree = ""; }; AC0404951BFACDC900AC1501 /* RidesClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesClient.swift; sourceTree = ""; }; AC0404991BFACE5400AC1501 /* RequestDeeplinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestDeeplinkTests.swift; sourceTree = ""; }; + B289FCA129D6123B0071FFF4 /* PrefillValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefillValue.swift; sourceTree = ""; }; + B291DED829EF09D1005DD810 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + B2D31CA329E88C4B001A9891 /* Par.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Par.swift; sourceTree = ""; }; C1C60712D3DA8E230F9B36D2 /* Pods-UberRidesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UberRidesTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-UberRidesTests/Pods-UberRidesTests.release.xcconfig"; sourceTree = ""; }; D80D4A511CD01894000EE8A9 /* AuthorizationCodeGrantAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeGrantAuthenticator.swift; sourceTree = ""; }; D81A8B151C658A2800339C13 /* TimeEstimate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimeEstimate.swift; path = Model/TimeEstimate.swift; sourceTree = ""; }; @@ -324,7 +331,6 @@ DCDD153B1D99A7F20053BC8F /* TestAppShim.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestAppShim.app; sourceTree = BUILT_PRODUCTS_DIR; }; DCDD153D1D99A7F20053BC8F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DCDD153F1D99A7F20053BC8F /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - DCDD15421D99A7F20053BC8F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; DCDD15441D99A7F20053BC8F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DCDD15471D99A7F20053BC8F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; DCDD15491D99A7F20053BC8F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -388,6 +394,8 @@ files = ( B261EBA429E4A973007CDCC3 /* OHHTTPStubs in Frameworks */, B261EBA629E4A973007CDCC3 /* OHHTTPStubsSwift in Frameworks */, + B289FC9F29D5FB560071FFF4 /* UberCore.framework in Frameworks */, + B289FCA029D5FB7C0071FFF4 /* UberRides.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -577,10 +585,10 @@ DCDD153C1D99A7F20053BC8F /* TestAppShim */ = { isa = PBXGroup; children = ( + B291DED729EF09D1005DD810 /* Main.storyboard */, DCDD154D1D99A82D0053BC8F /* TestAppShim.entitlements */, DCDD153D1D99A7F20053BC8F /* AppDelegate.swift */, DCDD153F1D99A7F20053BC8F /* ViewController.swift */, - DCDD15411D99A7F20053BC8F /* Main.storyboard */, DCDD15441D99A7F20053BC8F /* Assets.xcassets */, DCDD15461D99A7F20053BC8F /* LaunchScreen.storyboard */, DCDD15491D99A7F20053BC8F /* Info.plist */, @@ -637,7 +645,9 @@ DCAEA9931CEE91D000E6F239 /* AuthenticationURLUtility.swift */, DF021B501F998A1D004A7893 /* OAuthEndpoint.swift */, DC8D42A81CF4C95000C16D16 /* UberAppDelegate.swift */, + B2D31CA329E88C4B001A9891 /* Par.swift */, DC1AE1B81CF6BF3900B8CFCB /* LoginButton.swift */, + B289FCA129D6123B0071FFF4 /* PrefillValue.swift */, ); path = Authentication; sourceTree = ""; @@ -976,7 +986,7 @@ DFFCA8DC1F97F6860032E6BA /* testInfo.plist in Resources */, DCDD15481D99A7F20053BC8F /* LaunchScreen.storyboard in Resources */, DCDD15451D99A7F20053BC8F /* Assets.xcassets in Resources */, - DCDD15431D99A7F20053BC8F /* Main.storyboard in Resources */, + B291DED929EF09D1005DD810 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1100,7 +1110,9 @@ 77370828212B366800BB1C01 /* EatsNativeAuthenticator.swift in Sources */, DFFCA8CA1F96AADE0032E6BA /* BaseDeeplink.swift in Sources */, DFFCA8D51F96C1160032E6BA /* UberError.swift in Sources */, + B289FCA229D6123B0071FFF4 /* PrefillValue.swift in Sources */, DF0A44A91F998C7E002CFE5B /* ImplicitGrantAuthenticator.swift in Sources */, + B2D31CA429E88C4B001A9891 /* Par.swift in Sources */, DF0A44AA1F998C7E002CFE5B /* AuthorizationCodeGrantAuthenticator.swift in Sources */, 772E1168212DF15800056C63 /* EatsAppStoreDeeplink.swift in Sources */, 77370822212B000200BB1C01 /* RidesAuthenticationDeeplink.swift in Sources */, @@ -1170,6 +1182,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + B291DED729EF09D1005DD810 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B291DED829EF09D1005DD810 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; DC8FC3611CBDF4D000D58839 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -1181,14 +1201,6 @@ name = Localizable.strings; sourceTree = ""; }; - DCDD15411D99A7F20053BC8F /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - DCDD15421D99A7F20053BC8F /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; DCDD15461D99A7F20053BC8F /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( From e4b9d4d78501f6adce247765fa6ed83c7eac114e Mon Sep 17 00:00:00 2001 From: mohssen Date: Tue, 18 Apr 2023 11:23:19 -0700 Subject: [PATCH 2/2] [PAR] addressed comments --- .../Authentication/OAuthEndpoint.swift | 39 +++++++++---------- source/UberRidesTests/RidesClientTests.swift | 4 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/source/UberCore/Authentication/OAuthEndpoint.swift b/source/UberCore/Authentication/OAuthEndpoint.swift index 587f77ab..f8bd9dc7 100644 --- a/source/UberCore/Authentication/OAuthEndpoint.swift +++ b/source/UberCore/Authentication/OAuthEndpoint.swift @@ -61,7 +61,7 @@ public enum OAuth: APIEndpoint { components.queryItems = query return components.query?.data(using: String.Encoding.utf8) case .par(let clientID, let loginHint, let responseType): - let loginHintString = (try? JSONSerialization.data(withJSONObject: loginHint))?.base64EncodedString() ?? "" + let loginHintString = base64EncodedString(from: loginHint) ?? "" let query = queryBuilder( ("client_id", clientID), ("response_type", responseType.rawValue), @@ -96,34 +96,19 @@ public enum OAuth: APIEndpoint { switch self { case .implicitLogin(let clientID, let scopes, let redirect, let requestUri): var loginQuery = baseLoginQuery(clientID, redirect: redirect, scopes: scopes) - let additionalQueryItems: [URLQueryItem] = [ + let additionalQueryItems = buildQueryItems([ ("response_type", ResponseType.token.rawValue), ("request_uri", requestUri) - ] - .compactMap { pair -> [URLQueryItem]? in - guard let value = pair.1 else { - return nil - } - return self.queryBuilder((pair.0, value)) - } - .flatMap { $0 } - + ]) loginQuery.append(contentsOf: additionalQueryItems) return loginQuery case .authorizationCodeLogin(let clientID, let redirect, let scopes, let state, let requestUri): var loginQuery = baseLoginQuery(clientID, redirect: redirect, scopes: scopes) - let additionalQueryItems: [URLQueryItem] = [ + let additionalQueryItems = buildQueryItems([ ("response_type", ResponseType.code.rawValue), ("state", state ?? ""), ("request_uri", requestUri) - ] - .compactMap { pair -> [URLQueryItem]? in - guard let value = pair.1 else { - return nil - } - return self.queryBuilder((pair.0, value)) - } - .flatMap { $0 } + ]) loginQuery.append(contentsOf: additionalQueryItems) return loginQuery case .par: @@ -163,6 +148,20 @@ public enum OAuth: APIEndpoint { } } + private func base64EncodedString(from dict: [String: String]) -> String? { + (try? JSONSerialization.data(withJSONObject: dict))?.base64EncodedString() + } + + private func buildQueryItems(_ items: [(String, String?)]) -> [URLQueryItem] { + items.compactMap { pair -> [URLQueryItem]? in + guard let value = pair.1 else { + return nil + } + return self.queryBuilder((pair.0, value)) + } + .flatMap { $0 } + } + // MARK: - ResponseType public enum ResponseType: String { diff --git a/source/UberRidesTests/RidesClientTests.swift b/source/UberRidesTests/RidesClientTests.swift index 96450c3b..84b804fd 100644 --- a/source/UberRidesTests/RidesClientTests.swift +++ b/source/UberRidesTests/RidesClientTests.swift @@ -843,7 +843,7 @@ class RidesClientTests: XCTestCase { } func testRefreshToken() { - stub(condition: isHost("login.uber.com")) { _ in + stub(condition: isHost("auth.uber.com")) { _ in return HTTPStubsResponse(fileAtPath: OHPathForFile("refresh.json", type(of: self))!, statusCode: 200, headers: nil) } let refreshToken = "thisIsRefresh" @@ -875,7 +875,7 @@ class RidesClientTests: XCTestCase { } func testRefreshTokenInvalid() { - stub(condition: isHost("login.uber.com")) { _ in + stub(condition: isHost("auth.uber.com")) { _ in return HTTPStubsResponse(jsonObject: ["error":"invalid_refresh_token"], statusCode: 400, headers: nil) } let refreshToken = "thisIsRefresh"