Skip to content
This repository was archived by the owner on Nov 7, 2024. It is now read-only.

Commit 3127c7b

Browse files
Merge pull request #6 from AckeeCZ/user_profile
Load user's profile data
2 parents c6830dd + b1d25e0 commit 3127c7b

File tree

5 files changed

+140
-14
lines changed

5 files changed

+140
-14
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension JSONDecoder {
4+
/// Framework's setup of the `JSONDecoder`
5+
/// tailored for its use
6+
static let app: JSONDecoder = {
7+
let decoder = JSONDecoder()
8+
decoder.keyDecodingStrategy = .convertFromSnakeCase
9+
return decoder
10+
}()
11+
}

Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ public enum GoogleSignInError: Error, Equatable {
55
case authenticationError(Error)
66
case invalidCode
77
case invalidResponse
8-
case invalidTokenRequest
98
case networkError(Error)
109
case tokenDecodingError(Error)
1110
case userCancelledSignInFlow
11+
case noProfile(Error)
1212
}
1313

1414
public func == (lhs: Error, rhs: Error) -> Bool {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,54 @@
1+
import Foundation
2+
13
/// Google sign-in user account.
24
public struct GoogleUser: Codable, Equatable {
5+
6+
/// User's profile info.
7+
///
8+
/// All the properties are optional according
9+
/// to the [documentation](https://googleapis.dev/nodejs/googleapis/latest/oauth2/interfaces/Schema$Userinfo.html#info).
10+
public struct Profile: Codable, Equatable {
11+
12+
/// The obfuscated ID of the user.
13+
public let id: String?
14+
15+
/// The user's email address.
16+
public let email: String?
17+
18+
/// Boolean flag which is true if the email address is verified.
19+
/// Always verified because we only return the user's primary email address.
20+
public let verifiedEmail: Bool?
21+
22+
/// The user's full name.
23+
public let name: String?
24+
25+
/// The user's first name.
26+
public let givenName: String?
27+
28+
/// The user's last name.
29+
public let familyName: String?
30+
31+
/// The user's gender.
32+
public let gender: String?
33+
34+
/// URL of the profile page.
35+
public let link: URL?
36+
37+
/// URL of the user's picture image.
38+
public let picture: URL?
39+
40+
/// The hosted domain e.g. example.com if the user is Google apps user.
41+
public let hd: String?
42+
43+
/// The user's preferred locale.
44+
public let locale: String?
45+
}
46+
347
public let accessToken: String
448
public let expiresIn: Int
549
public let idToken: String
650
public let refreshToken: String?
751
public let scope: String
852
public let tokenType: String
53+
public var profile: Profile?
954
}

Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public final class OpenGoogleSignIn: NSObject {
4343

4444
/// Google API OAuth 2.0 token url.
4545
private static let tokenURL: URL? = URL(string: "https://www.googleapis.com/oauth2/v4/token")
46+
47+
/// Google API profile url
48+
private static let profileURL: URL? = URL(string: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json")
4649

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

137140
/// Decodes `GoogleUser` from OAuth 2.0 response.
138141
private func decodeUser(from data: Data) throws -> GoogleUser {
139-
let decoder = JSONDecoder()
140-
decoder.keyDecodingStrategy = .convertFromSnakeCase
141-
142-
return try decoder.decode(GoogleUser.self, from: data)
142+
try JSONDecoder.app.decode(GoogleUser.self, from: data)
143143
}
144144

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

152155
guard let tokenRequest = makeTokenRequest(with: code) else {
153-
completion(.failure(.invalidTokenRequest))
156+
assertionFailure("Invalid token request")
157+
return
158+
}
159+
160+
makeRequest(tokenRequest) { result in
161+
switch result {
162+
case let .success(data):
163+
do {
164+
let user = try self.decodeUser(from: data)
165+
self.fetchProfile(user: user, completion: completion)
166+
} catch {
167+
completion(.failure(.tokenDecodingError(error)))
168+
}
169+
170+
case let .failure(error):
171+
completion(.failure(error))
172+
}
173+
}
174+
}
175+
176+
private func fetchProfile(user: GoogleUser, completion: @escaping (Result<GoogleUser, GoogleSignInError>) -> Void) {
177+
guard let profileRequest = makeProfileRequest(user: user) else {
178+
assertionFailure("Invalid profile request")
154179
return
155180
}
156181

157-
let task = session.dataTask(with: tokenRequest) { data, response, error in
182+
var user = user
183+
184+
makeRequest(profileRequest) { result in
185+
switch result {
186+
case let .success(data):
187+
let profile = try? JSONDecoder.app.decode(GoogleUser.Profile.self, from: data)
188+
user.profile = profile
189+
completion(.success(user))
190+
191+
case let .failure(error):
192+
completion(.failure(.noProfile(error)))
193+
}
194+
}
195+
}
196+
197+
/// Wrapper for easier `URLRequest` handling.
198+
private func makeRequest(_ request: URLRequest, completion: @escaping (Result<Data, GoogleSignInError>) -> Void) {
199+
let task = session.dataTask(with: request) { data, response, error in
158200
if let error = error {
159201
completion(.failure(.networkError(error)))
160202
return
@@ -165,11 +207,7 @@ public final class OpenGoogleSignIn: NSObject {
165207
return
166208
}
167209

168-
do {
169-
completion(.success(try self.decodeUser(from: data)))
170-
} catch {
171-
completion(.failure(.tokenDecodingError(error)))
172-
}
210+
completion(.success(data))
173211
}
174212
task.resume()
175213
}
@@ -182,7 +220,7 @@ public final class OpenGoogleSignIn: NSObject {
182220
}
183221

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

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

206244
return request
207245
}
246+
247+
/// Returns `URLRequest` to retrieve user's profile data
248+
func makeProfileRequest(user: GoogleUser) -> URLRequest? {
249+
guard let profileURL = OpenGoogleSignIn.profileURL else { return nil }
250+
251+
var request = URLRequest(url: profileURL)
252+
request.setValue("Bearer " + user.accessToken, forHTTPHeaderField: "Authorization")
253+
254+
return request
255+
}
208256
}
209257

210258
// MARK: - ASWebAuthenticationPresentationContextProviding

Tests/OpenGoogleSignInSDKTests/OpenGoogleSignInSDKTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,28 @@ final class OpenGoogleSignInTests: XCTestCase {
7474
XCTAssertNotNil(mockDelegate.user)
7575
}
7676

77+
// Since `URL` has optional initializer we need to
78+
// check if the URL provided by us is valid and
79+
// therefore we don't need to worry about this edge case.
80+
func test_tokenRequest_isValid() {
81+
// When
82+
let request = sharedInstance.makeTokenRequest(with: "code")
83+
84+
// Then
85+
XCTAssertNotNil(request)
86+
}
87+
88+
// Since `URL` has optional initializer we need to
89+
// check if the URL provided by us is valid and
90+
// therefore we don't need to worry about this edge case.
91+
func test_profileRequest_isValid() {
92+
// Given
93+
let user = mockUser()
94+
95+
// Then
96+
XCTAssertNotNil(sharedInstance.makeProfileRequest(user: user))
97+
}
98+
7799
// MARK: - Private helpers
78100

79101
private func mockUser() -> GoogleUser {

0 commit comments

Comments
 (0)