Skip to content

Commit

Permalink
Merge pull request AckeeCZ#6 from AckeeCZ/user_profile
Browse files Browse the repository at this point in the history
Load user's profile data
  • Loading branch information
LukasHromadnik authored Jul 8, 2021
2 parents c6830dd + b1d25e0 commit 3127c7b
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 14 deletions.
11 changes: 11 additions & 0 deletions Sources/OpenGoogleSignInSDK/JSONDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

extension JSONDecoder {
/// Framework's setup of the `JSONDecoder`
/// tailored for its use
static let app: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
}
2 changes: 1 addition & 1 deletion Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ public enum GoogleSignInError: Error, Equatable {
case authenticationError(Error)
case invalidCode
case invalidResponse
case invalidTokenRequest
case networkError(Error)
case tokenDecodingError(Error)
case userCancelledSignInFlow
case noProfile(Error)
}

public func == (lhs: Error, rhs: Error) -> Bool {
Expand Down
45 changes: 45 additions & 0 deletions Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,54 @@
import Foundation

/// Google sign-in user account.
public struct GoogleUser: Codable, Equatable {

/// User's profile info.
///
/// All the properties are optional according
/// to the [documentation](https://googleapis.dev/nodejs/googleapis/latest/oauth2/interfaces/Schema$Userinfo.html#info).
public struct Profile: Codable, Equatable {

/// The obfuscated ID of the user.
public let id: String?

/// The user's email address.
public let email: String?

/// Boolean flag which is true if the email address is verified.
/// Always verified because we only return the user's primary email address.
public let verifiedEmail: Bool?

/// The user's full name.
public let name: String?

/// The user's first name.
public let givenName: String?

/// The user's last name.
public let familyName: String?

/// The user's gender.
public let gender: String?

/// URL of the profile page.
public let link: URL?

/// URL of the user's picture image.
public let picture: URL?

/// The hosted domain e.g. example.com if the user is Google apps user.
public let hd: String?

/// The user's preferred locale.
public let locale: String?
}

public let accessToken: String
public let expiresIn: Int
public let idToken: String
public let refreshToken: String?
public let scope: String
public let tokenType: String
public var profile: Profile?
}
74 changes: 61 additions & 13 deletions Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public final class OpenGoogleSignIn: NSObject {

/// Google API OAuth 2.0 token url.
private static let tokenURL: URL? = URL(string: "https://www.googleapis.com/oauth2/v4/token")

/// Google API profile url
private static let profileURL: URL? = URL(string: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json")

/// The client's redirect URI, which is based on `clientID`.
private var redirectURI: String {
Expand Down Expand Up @@ -136,25 +139,64 @@ public final class OpenGoogleSignIn: NSObject {

/// Decodes `GoogleUser` from OAuth 2.0 response.
private func decodeUser(from data: Data) throws -> GoogleUser {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

return try decoder.decode(GoogleUser.self, from: data)
try JSONDecoder.app.decode(GoogleUser.self, from: data)
}

/// Handles OAuth 2.0 token response.
///
/// After successful authentication we made another request
/// to obtain user's profile data, e.g. email and name.
private func handleTokenResponse(using redirectUrl: URL, completion: @escaping (Result<GoogleUser, GoogleSignInError>) -> Void) {
guard let code = self.parseCode(from: redirectUrl) else {
guard let code = parseCode(from: redirectUrl) else {
completion(.failure(.invalidCode))
return
}

guard let tokenRequest = makeTokenRequest(with: code) else {
completion(.failure(.invalidTokenRequest))
assertionFailure("Invalid token request")
return
}

makeRequest(tokenRequest) { result in
switch result {
case let .success(data):
do {
let user = try self.decodeUser(from: data)
self.fetchProfile(user: user, completion: completion)
} catch {
completion(.failure(.tokenDecodingError(error)))
}

case let .failure(error):
completion(.failure(error))
}
}
}

private func fetchProfile(user: GoogleUser, completion: @escaping (Result<GoogleUser, GoogleSignInError>) -> Void) {
guard let profileRequest = makeProfileRequest(user: user) else {
assertionFailure("Invalid profile request")
return
}

let task = session.dataTask(with: tokenRequest) { data, response, error in
var user = user

makeRequest(profileRequest) { result in
switch result {
case let .success(data):
let profile = try? JSONDecoder.app.decode(GoogleUser.Profile.self, from: data)
user.profile = profile
completion(.success(user))

case let .failure(error):
completion(.failure(.noProfile(error)))
}
}
}

/// Wrapper for easier `URLRequest` handling.
private func makeRequest(_ request: URLRequest, completion: @escaping (Result<Data, GoogleSignInError>) -> Void) {
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(.networkError(error)))
return
Expand All @@ -165,11 +207,7 @@ public final class OpenGoogleSignIn: NSObject {
return
}

do {
completion(.success(try self.decodeUser(from: data)))
} catch {
completion(.failure(.tokenDecodingError(error)))
}
completion(.success(data))
}
task.resume()
}
Expand All @@ -182,7 +220,7 @@ public final class OpenGoogleSignIn: NSObject {
}

/// Returns `URLRequest` to retrieve Google sign-in OAuth 2.0 token using arameters provided by the app.
private func makeTokenRequest(with code: String) -> URLRequest? {
func makeTokenRequest(with code: String) -> URLRequest? {
guard let tokenURL = OpenGoogleSignIn.tokenURL else { return nil }

var request = URLRequest(url: tokenURL)
Expand All @@ -205,6 +243,16 @@ public final class OpenGoogleSignIn: NSObject {

return request
}

/// Returns `URLRequest` to retrieve user's profile data
func makeProfileRequest(user: GoogleUser) -> URLRequest? {
guard let profileURL = OpenGoogleSignIn.profileURL else { return nil }

var request = URLRequest(url: profileURL)
request.setValue("Bearer " + user.accessToken, forHTTPHeaderField: "Authorization")

return request
}
}

// MARK: - ASWebAuthenticationPresentationContextProviding
Expand Down
22 changes: 22 additions & 0 deletions Tests/OpenGoogleSignInSDKTests/OpenGoogleSignInSDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,28 @@ final class OpenGoogleSignInTests: XCTestCase {
XCTAssertNotNil(mockDelegate.user)
}

// Since `URL` has optional initializer we need to
// check if the URL provided by us is valid and
// therefore we don't need to worry about this edge case.
func test_tokenRequest_isValid() {
// When
let request = sharedInstance.makeTokenRequest(with: "code")

// Then
XCTAssertNotNil(request)
}

// Since `URL` has optional initializer we need to
// check if the URL provided by us is valid and
// therefore we don't need to worry about this edge case.
func test_profileRequest_isValid() {
// Given
let user = mockUser()

// Then
XCTAssertNotNil(sharedInstance.makeProfileRequest(user: user))
}

// MARK: - Private helpers

private func mockUser() -> GoogleUser {
Expand Down

0 comments on commit 3127c7b

Please sign in to comment.