From fa2d885461a932c0259a001ba38648062e9eac31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=CC=81s=CC=8C=20Hromadni=CC=81k?= Date: Thu, 1 Jul 2021 18:06:42 +0200 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Add=20user=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/GoogleUser.swift | 15 +++++ .../OpenGoogleSignInSDK.swift | 56 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift b/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift index 571d6b5..5e391a7 100644 --- a/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift +++ b/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift @@ -1,9 +1,24 @@ +import Foundation + /// Google sign-in user account. public struct GoogleUser: Codable, Equatable { + public struct Profile: Codable, Equatable { + public let id: String + public let email: String + public let verifiedEmail: Bool + public let name: String + public let givenName: String + public let familyName: String + public let picture: URL + public let locale: String + public let hd: 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? } diff --git a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift index 859db04..2e6cdb4 100644 --- a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift +++ b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift @@ -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 { @@ -148,7 +151,42 @@ public final class OpenGoogleSignIn: NSObject { return } - let task = session.dataTask(with: tokenRequest) { data, response, error in + makeRequest(tokenRequest) { result in + switch result { + case let .success(data): + do { + var user = try self.decodeUser(from: data) + + guard let profileRequest = self.makeProfileRequest(user: user) else { + completion(.success(user)) + return + } + + self.makeRequest(profileRequest) { result in + switch result { + case let .success(data): + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let profile = try? decoder.decode(GoogleUser.Profile.self, from: data) + user.profile = profile + completion(.success(user)) + + case let .failure(error): + completion(.failure(error)) + } + } + } catch { + completion(.failure(.tokenDecodingError(error))) + } + + case let .failure(error): + completion(.failure(error)) + } + } + } + + private func makeRequest(_ request: URLRequest, completion: @escaping (Result) -> Void) { + let task = session.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(.networkError(error))) return @@ -159,11 +197,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() } @@ -199,6 +233,16 @@ public final class OpenGoogleSignIn: NSObject { return request } + + /// Returns `URLRequest` to retrieve user's profile data + private 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 From c5b9b7af7a94b3f0eb752555c2fe0d514d765a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=CC=81s=CC=8C=20Hromadni=CC=81k?= Date: Fri, 2 Jul 2021 08:34:27 +0200 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=9D=20Add=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/GoogleSignInError.swift | 23 ++++++++- .../Model/GoogleUser.swift | 48 +++++++++++++++---- .../OpenGoogleSignInSDK.swift | 8 +++- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift b/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift index aab88c3..a872abc 100644 --- a/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift +++ b/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift @@ -1,7 +1,7 @@ import Foundation /// Google sign-in error. -public enum GoogleSignInError: Error, Equatable { +public enum GoogleSignInError: Equatable { case authenticationError(Error) case invalidCode case invalidResponse @@ -11,6 +11,27 @@ public enum GoogleSignInError: Error, Equatable { case userCancelledSignInFlow } +extension GoogleSignInError: LocalizedError { + public var errorDescription: String? { + switch self { + case let .authenticationError(error): + return "authenticationError, underlying error \(error.localizedDescription)" + case .invalidCode: + return "invalidCode" + case .invalidResponse: + return "invalidResponse" + case .invalidTokenRequest: + return "invalidTokenRequest" + case let .networkError(error): + return "network, underlying error \(error.localizedDescription)" + case let .tokenDecodingError(error): + return "tokenDecoding, underlying error \(error.localizedDescription)" + case .userCancelledSignInFlow: + return "userCancelledSignInFlow" + } + } +} + public func == (lhs: Error, rhs: Error) -> Bool { guard type(of: lhs) == type(of: rhs) else { return false } let error1 = lhs as NSError diff --git a/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift b/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift index 5e391a7..8ccda98 100644 --- a/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift +++ b/Sources/OpenGoogleSignInSDK/Model/GoogleUser.swift @@ -2,16 +2,46 @@ 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 { - public let id: String - public let email: String - public let verifiedEmail: Bool - public let name: String - public let givenName: String - public let familyName: String - public let picture: URL - public let locale: String - public let hd: String + + /// 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 diff --git a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift index 2e6cdb4..85ec9e4 100644 --- a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift +++ b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift @@ -162,6 +162,10 @@ public final class OpenGoogleSignIn: NSObject { return } + // To obtain profile data we need to make another request. + // If the request fails, we only log the error + // and call completion with the `GoogleUser` object + // without his profile info. self.makeRequest(profileRequest) { result in switch result { case let .success(data): @@ -172,7 +176,8 @@ public final class OpenGoogleSignIn: NSObject { completion(.success(user)) case let .failure(error): - completion(.failure(error)) + os_log(.error, "Profile request failed with error: %@", error.localizedDescription) + completion(.success(user)) } } } catch { @@ -185,6 +190,7 @@ public final class OpenGoogleSignIn: NSObject { } } + /// Wrapper for easier `URLRequest` handling. private func makeRequest(_ request: URLRequest, completion: @escaping (Result) -> Void) { let task = session.dataTask(with: request) { data, response, error in if let error = error { From 0a85973547f1ae9973aee8bec1308a06d9b7b565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=CC=81s=CC=8C=20Hromadni=CC=81k?= Date: Fri, 2 Jul 2021 13:23:13 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=8E=A8=20Raise=20error=20when=20profi?= =?UTF-8?q?le=20is=20not=20fetched?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/GoogleSignInError.swift | 24 ++----------------- .../OpenGoogleSignInSDK.swift | 10 ++++---- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift b/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift index a872abc..178b742 100644 --- a/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift +++ b/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift @@ -1,7 +1,7 @@ import Foundation /// Google sign-in error. -public enum GoogleSignInError: Equatable { +public enum GoogleSignInError: Error, Equatable { case authenticationError(Error) case invalidCode case invalidResponse @@ -9,27 +9,7 @@ public enum GoogleSignInError: Equatable { case networkError(Error) case tokenDecodingError(Error) case userCancelledSignInFlow -} - -extension GoogleSignInError: LocalizedError { - public var errorDescription: String? { - switch self { - case let .authenticationError(error): - return "authenticationError, underlying error \(error.localizedDescription)" - case .invalidCode: - return "invalidCode" - case .invalidResponse: - return "invalidResponse" - case .invalidTokenRequest: - return "invalidTokenRequest" - case let .networkError(error): - return "network, underlying error \(error.localizedDescription)" - case let .tokenDecodingError(error): - return "tokenDecoding, underlying error \(error.localizedDescription)" - case .userCancelledSignInFlow: - return "userCancelledSignInFlow" - } - } + case noProfile(Error) } public func == (lhs: Error, rhs: Error) -> Bool { diff --git a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift index 85ec9e4..e9ab18d 100644 --- a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift +++ b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift @@ -140,6 +140,9 @@ public final class OpenGoogleSignIn: NSObject { } /// 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) -> Void) { guard let code = self.parseCode(from: redirectUrl) else { completion(.failure(.invalidCode)) @@ -162,10 +165,6 @@ public final class OpenGoogleSignIn: NSObject { return } - // To obtain profile data we need to make another request. - // If the request fails, we only log the error - // and call completion with the `GoogleUser` object - // without his profile info. self.makeRequest(profileRequest) { result in switch result { case let .success(data): @@ -176,8 +175,7 @@ public final class OpenGoogleSignIn: NSObject { completion(.success(user)) case let .failure(error): - os_log(.error, "Profile request failed with error: %@", error.localizedDescription) - completion(.success(user)) + completion(.failure(.noProfile(error))) } } } catch { From 3cad66c6d15dbe3136d77c2031bcfd369eb50601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=CC=81s=CC=8C=20Hromadni=CC=81k?= Date: Thu, 8 Jul 2021 06:16:28 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=8E=A8=20Use=20app=20decoder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/OpenGoogleSignInSDK/JSONDecoder.swift | 11 +++++++++++ Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift | 9 ++------- 2 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 Sources/OpenGoogleSignInSDK/JSONDecoder.swift diff --git a/Sources/OpenGoogleSignInSDK/JSONDecoder.swift b/Sources/OpenGoogleSignInSDK/JSONDecoder.swift new file mode 100644 index 0000000..5684f0a --- /dev/null +++ b/Sources/OpenGoogleSignInSDK/JSONDecoder.swift @@ -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 + }() +} diff --git a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift index e9ab18d..a333e43 100644 --- a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift +++ b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift @@ -133,10 +133,7 @@ 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. @@ -168,9 +165,7 @@ public final class OpenGoogleSignIn: NSObject { self.makeRequest(profileRequest) { result in switch result { case let .success(data): - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - let profile = try? decoder.decode(GoogleUser.Profile.self, from: data) + let profile = try? JSONDecoder.app.decode(GoogleUser.Profile.self, from: data) user.profile = profile completion(.success(user)) From 8ff7f193e45a876c96916f61415f41a85f3e8754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=CC=81s=CC=8C=20Hromadni=CC=81k?= Date: Thu, 8 Jul 2021 06:20:21 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=8E=A8=20Use=20string=20concatenation?= =?UTF-8?q?=20instead=20of=20interpolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift index a333e43..976d02d 100644 --- a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift +++ b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift @@ -238,7 +238,7 @@ public final class OpenGoogleSignIn: NSObject { guard let profileURL = OpenGoogleSignIn.profileURL else { return nil } var request = URLRequest(url: profileURL) - request.setValue("Bearer \(user.accessToken)", forHTTPHeaderField: "Authorization") + request.setValue("Bearer " + user.accessToken, forHTTPHeaderField: "Authorization") return request } From 5b1f23e73d341a8434bd8007a35a343eb7d28542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=CC=81s=CC=8C=20Hromadni=CC=81k?= Date: Thu, 8 Jul 2021 06:55:46 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=8E=A8=20Make=20profile=20request=20o?= =?UTF-8?q?nly=20when=20scopes=20are=20provided?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/GoogleSignInError.swift | 1 - .../OpenGoogleSignInSDK.swift | 48 +++++++++++-------- .../OpenGoogleSignInSDKTests.swift | 22 +++++++++ 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift b/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift index 178b742..f9724be 100644 --- a/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift +++ b/Sources/OpenGoogleSignInSDK/Model/GoogleSignInError.swift @@ -5,7 +5,6 @@ public enum GoogleSignInError: Error, Equatable { case authenticationError(Error) case invalidCode case invalidResponse - case invalidTokenRequest case networkError(Error) case tokenDecodingError(Error) case userCancelledSignInFlow diff --git a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift index 976d02d..022f863 100644 --- a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift +++ b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift @@ -141,13 +141,13 @@ public final class OpenGoogleSignIn: NSObject { /// 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) -> 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 } @@ -155,23 +155,12 @@ public final class OpenGoogleSignIn: NSObject { switch result { case let .success(data): do { - var user = try self.decodeUser(from: data) + let user = try self.decodeUser(from: data) - guard let profileRequest = self.makeProfileRequest(user: user) else { + if self.scopes.contains(.email) || self.scopes.contains(.profile) { + self.fetchProfile(user: user, completion: completion) + } else { completion(.success(user)) - return - } - - self.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))) - } } } catch { completion(.failure(.tokenDecodingError(error))) @@ -182,6 +171,27 @@ public final class OpenGoogleSignIn: NSObject { } } } + + private func fetchProfile(user: GoogleUser, completion: @escaping (Result) -> Void) { + guard let profileRequest = makeProfileRequest(user: user) else { + assertionFailure("Invalid profile request") + return + } + + 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) -> Void) { @@ -209,7 +219,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) @@ -234,7 +244,7 @@ public final class OpenGoogleSignIn: NSObject { } /// Returns `URLRequest` to retrieve user's profile data - private func makeProfileRequest(user: GoogleUser) -> URLRequest? { + func makeProfileRequest(user: GoogleUser) -> URLRequest? { guard let profileURL = OpenGoogleSignIn.profileURL else { return nil } var request = URLRequest(url: profileURL) diff --git a/Tests/OpenGoogleSignInSDKTests/OpenGoogleSignInSDKTests.swift b/Tests/OpenGoogleSignInSDKTests/OpenGoogleSignInSDKTests.swift index 8154250..b22cce0 100644 --- a/Tests/OpenGoogleSignInSDKTests/OpenGoogleSignInSDKTests.swift +++ b/Tests/OpenGoogleSignInSDKTests/OpenGoogleSignInSDKTests.swift @@ -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 { From b1d25e012a3b42470d5401511727acd7f2555cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=CC=81s=CC=8C=20Hromadni=CC=81k?= Date: Thu, 8 Jul 2021 07:27:01 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=8E=A8=20Fetch=20profile=20every=20ti?= =?UTF-8?q?me=20after=20authorization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift index 022f863..fc3eda8 100644 --- a/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift +++ b/Sources/OpenGoogleSignInSDK/OpenGoogleSignInSDK.swift @@ -156,12 +156,7 @@ public final class OpenGoogleSignIn: NSObject { case let .success(data): do { let user = try self.decodeUser(from: data) - - if self.scopes.contains(.email) || self.scopes.contains(.profile) { - self.fetchProfile(user: user, completion: completion) - } else { - completion(.success(user)) - } + self.fetchProfile(user: user, completion: completion) } catch { completion(.failure(.tokenDecodingError(error))) }