diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift index 7692cb22..59bdee2e 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift @@ -17,8 +17,8 @@ import Combine import GoogleSignIn -/// An observable class to load the current user's birthday. -final class BirthdayLoader: ObservableObject { +/// A class to load the current user's birthday. +final class BirthdayLoader { /// The scope required to read a user's birthday. static let birthdayReadScope = "https://www.googleapis.com/auth/user.birthday.read" private let baseUrlString = "https://people.googleapis.com/v1/people/me" @@ -38,74 +38,48 @@ final class BirthdayLoader: ObservableObject { return URLRequest(url: url) }() - private lazy var session: URLSession? = { - guard let accessToken = GIDSignIn - .sharedInstance - .currentUser? - .accessToken - .tokenString else { return nil } + private func sessionWithFreshToken() async throws -> URLSession { + guard let user = GIDSignIn.sharedInstance.currentUser else { + throw Error.noCurrentUserForSessionWithFreshToken + } + try await user.refreshTokensIfNeeded() let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = [ - "Authorization": "Bearer \(accessToken)" + "Authorization": "Bearer \(user.accessToken.tokenString)" ] - return URLSession(configuration: configuration) - }() - - private func sessionWithFreshToken(completion: @escaping (Result) -> Void) { - GIDSignIn.sharedInstance.currentUser?.refreshTokensIfNeeded { user, error in - guard let token = user?.accessToken.tokenString else { - completion(.failure(.couldNotCreateURLSession(error))) - return - } - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = [ - "Authorization": "Bearer \(token)" - ] - let session = URLSession(configuration: configuration) - completion(.success(session)) - } + let session = URLSession(configuration: configuration) + return session } - /// Creates a `Publisher` to fetch a user's `Birthday`. - /// - parameter completion: A closure passing back the `AnyPublisher` - /// upon success. - /// - note: The `AnyPublisher` passed back through the `completion` closure is created with a - /// fresh token. See `sessionWithFreshToken(completion:)` for more details. - func birthdayPublisher(completion: @escaping (AnyPublisher) -> Void) { - sessionWithFreshToken { [weak self] result in - switch result { - case .success(let authSession): - guard let request = self?.request else { - return completion(Fail(error: .couldNotCreateURLRequest).eraseToAnyPublisher()) + /// Fetches a `Birthday`. + /// - returns An instance of `Birthday`. + /// - throws: An instance of `BirthdayLoader.Error` arising while fetching a birthday. + func loadBirthday() async throws -> Birthday { + let session = try await sessionWithFreshToken() + guard let request = request else { + throw Error.couldNotCreateURLRequest + } + let birthdayData = try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation) -> Void in + let task = session.dataTask(with: request) { data, response, error in + guard let data = data else { + return continuation.resume(throwing: error ?? Error.noBirthdayData) } - let bdayPublisher = authSession.dataTaskPublisher(for: request) - .tryMap { data, error -> Birthday in - let decoder = JSONDecoder() - let birthdayResponse = try decoder.decode(BirthdayResponse.self, from: data) - return birthdayResponse.firstBirthday - } - .mapError { error -> Error in - guard let loaderError = error as? Error else { - return Error.couldNotFetchBirthday(underlying: error) - } - return loaderError - } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - completion(bdayPublisher) - case .failure(let error): - completion(Fail(error: error).eraseToAnyPublisher()) + continuation.resume(returning: data) } + task.resume() } + let decoder = JSONDecoder() + let birthdayResponse = try decoder.decode(BirthdayResponse.self, from: birthdayData) + return birthdayResponse.firstBirthday } } extension BirthdayLoader { - /// An error representing what went wrong in fetching a user's number of day until their birthday. + /// An error for what went wrong in fetching a user's number of days until their birthday. enum Error: Swift.Error { - case couldNotCreateURLSession(Swift.Error?) + case noCurrentUserForSessionWithFreshToken case couldNotCreateURLRequest - case userHasNoBirthday - case couldNotFetchBirthday(underlying: Swift.Error) + case noBirthdayData } } diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift index deacb35b..8cd70cc2 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -16,9 +16,14 @@ import Foundation import GoogleSignIn +#if os(iOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif -/// An observable class for authenticating via Google. -final class GoogleSignInAuthenticator: ObservableObject { +/// A class for authenticating via Google. +final class GoogleSignInAuthenticator { private var authViewModel: AuthenticationViewModel /// Creates an instance of this authenticator. @@ -27,38 +32,27 @@ final class GoogleSignInAuthenticator: ObservableObject { self.authViewModel = authViewModel } - /// Signs in the user based upon the selected account.' - /// - note: Successful calls to this will set the `authViewModel`'s `state` property. - func signIn() { #if os(iOS) - guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else { - print("There is no root view controller!") - return - } - - GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { signInResult, error in - guard let signInResult = signInResult else { - print("Error! \(String(describing: error))") - return - } - self.authViewModel.state = .signedIn(signInResult.user) - } - -#elseif os(macOS) - guard let presentingWindow = NSApplication.shared.windows.first else { - print("There is no presenting window!") - return - } - - GIDSignIn.sharedInstance.signIn(withPresenting: presentingWindow) { signInResult, error in - guard let signInResult = signInResult else { - print("Error! \(String(describing: error))") - return - } - self.authViewModel.state = .signedIn(signInResult.user) - } + /// Signs in the user based upon the selected account. + /// - parameter rootViewController: The `UIViewController` to use during the sign in flow. + /// - returns: The `GIDSignInResult`. + /// - throws: Any error that may arise during the sign in process. + @MainActor + func signIn(with rootViewController: UIViewController) async throws -> GIDSignInResult { + return try await GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) + } #endif + +#if os(macOS) + /// Signs in the user based upon the selected account. + /// - parameter window: The `NSWindow` to use during the sign in flow. + /// - returns: The `GIDSignInResult`. + /// - throws: Any error that may arise during the sign in process. + @MainActor + func signIn(with window: NSWindow) async throws -> GIDSignInResult { + return try await GIDSignIn.sharedInstance.signIn(withPresenting: window) } +#endif /// Signs out the current user. func signOut() { @@ -67,61 +61,51 @@ final class GoogleSignInAuthenticator: ObservableObject { } /// Disconnects the previously granted scope and signs the user out. - func disconnect() { - GIDSignIn.sharedInstance.disconnect { error in - if let error = error { - print("Encountered error disconnecting scope: \(error).") - } - self.signOut() - } + @MainActor + func disconnect() async throws { + try await GIDSignIn.sharedInstance.disconnect() + authViewModel.state = .signedOut } - // Confines birthday calucation to iOS for now. +#if os(iOS) /// Adds the birthday read scope for the current user. - /// - parameter completion: An escaping closure that is called upon successful completion of the - /// `addScopes(_:presenting:)` request. - /// - note: Successful requests will update the `authViewModel.state` with a new current user that - /// has the granted scope. - func addBirthdayReadScope(completion: @escaping () -> Void) { + /// - parameter viewController: The `UIViewController` to use while authorizing the scope. + /// - returns: The `GIDSignInResult`. + /// - throws: Any error that may arise while authorizing the scope. + @MainActor + func addBirthdayReadScope(viewController: UIViewController) async throws -> GIDSignInResult { guard let currentUser = GIDSignIn.sharedInstance.currentUser else { - fatalError("No user signed in!") - } - - #if os(iOS) - guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else { - fatalError("No root view controller!") - } - - currentUser.addScopes([BirthdayLoader.birthdayReadScope], - presenting: rootViewController) { signInResult, error in - if let error = error { - print("Found error while adding birthday read scope: \(error).") - return - } - - guard let signInResult = signInResult else { return } - self.authViewModel.state = .signedIn(signInResult.user) - completion() - } - - #elseif os(macOS) - guard let presentingWindow = NSApplication.shared.windows.first else { - fatalError("No presenting window!") + fatalError("No currentUser!") } + return try await currentUser.addScopes( + [BirthdayLoader.birthdayReadScope], + presenting: viewController + ) + } +#endif - currentUser.addScopes([BirthdayLoader.birthdayReadScope], - presenting: presentingWindow) { signInResult, error in - if let error = error { - print("Found error while adding birthday read scope: \(error).") - return - } - - guard let signInResult = signInResult else { return } - self.authViewModel.state = .signedIn(signInResult.user) - completion() +#if os(macOS) + /// Adds the birthday read scope for the current user. + /// - parameter window: The `NSWindow` to use while authorizing the scope. + /// - returns: The `GIDSignInResult`. + /// - throws: Any error that may arise while authorizing the scope. + @MainActor + func addBirthdayReadScope(window: NSWindow) async throws -> GIDSignInResult { + guard let currentUser = GIDSignIn.sharedInstance.currentUser else { + fatalError("No currentUser!") } - - #endif + return try await currentUser.addScopes( + [BirthdayLoader.birthdayReadScope], + presenting: window + ) } +#endif +} +extension GoogleSignInAuthenticator { + enum Error: Swift.Error { + case failedToSignIn + case failedToAddBirthdayReadScope(Swift.Error) + case userUnexpectedlyNilWhileAddingBirthdayReadScope + } } diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift index 3ad14289..9d16e0b2 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift @@ -47,7 +47,30 @@ final class AuthenticationViewModel: ObservableObject { /// Signs the user in. func signIn() { - authenticator.signIn() +#if os(iOS) + guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else { + print("There is no root view controller!") + return + } +#elseif os(macOS) + guard let presentingWindow = NSApplication.shared.windows.first else { + print("There is no presenting window!") + return + } +#endif + + Task { @MainActor in + do { +#if os(iOS) + let signInResult = try await authenticator.signIn(with: rootViewController) +#elseif os(macOS) + let signInResult = try await authenticator.signIn(with: presentingWindow) +#endif + self.state = .signedIn(signInResult.user) + } catch { + print("Error signing in: \(error)") + } + } } /// Signs the user out. @@ -55,21 +78,41 @@ final class AuthenticationViewModel: ObservableObject { authenticator.signOut() } - /// Disconnects the previously granted scope and logs the user out. + /// Disconnects the previously granted scope and signs the user out. func disconnect() { - authenticator.disconnect() + Task { @MainActor in + do { + try await authenticator.disconnect() + } catch { + print("Error disconnecting: \(error)") + } + } } var hasBirthdayReadScope: Bool { return authorizedScopes.contains(BirthdayLoader.birthdayReadScope) } +#if os(iOS) /// Adds the requested birthday read scope. - /// - parameter completion: An escaping closure that is called upon successful completion. - func addBirthdayReadScope(completion: @escaping () -> Void) { - authenticator.addBirthdayReadScope(completion: completion) + /// - parameter viewController: A `UIViewController` to use while presenting the flow. + /// - returns: The `GIDSignInResult`. + /// - throws: Any error that may arise while adding the read birthday scope. + func addBirthdayReadScope(viewController: UIViewController) async throws -> GIDSignInResult { + return try await authenticator.addBirthdayReadScope(viewController: viewController) } +#endif + +#if os(macOS) + /// adds the requested birthday read scope. + /// - parameter window: An `NSWindow` to use while presenting the flow. + /// - returns: The `GIDSignInResult`. + /// - throws: Any error that may arise while adding the read birthday scope. + func addBirthdayReadScope(window: NSWindow) async throws -> GIDSignInResult { + return try await authenticator.addBirthdayReadScope(window: window) + } +#endif } extension AuthenticationViewModel { diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/BirthdayViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/BirthdayViewModel.swift index 598ce1c4..db98b539 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/BirthdayViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/BirthdayViewModel.swift @@ -17,7 +17,8 @@ import Combine import Foundation -/// An observable class representing the current user's `Birthday` and the number of days until that date. +/// An observable class representing the current user's `Birthday` and the number of days until that +/// date. final class BirthdayViewModel: ObservableObject { /// The `Birthday` of the current user. /// - note: Changes to this property will be published to observers. @@ -40,17 +41,12 @@ final class BirthdayViewModel: ObservableObject { /// Fetches the birthday of the current user. func fetchBirthday() { - birthdayLoader.birthdayPublisher { publisher in - self.cancellable = publisher.sink { completion in - switch completion { - case .finished: - break - case .failure(let error): - self.birthday = Birthday.noBirthday - print("Error retrieving birthday: \(error)") - } - } receiveValue: { birthday in - self.birthday = birthday + Task { @MainActor in + do { + self.birthday = try await birthdayLoader.loadBirthday() + } catch { + print("Error retrieving birthday: \(error)") + self.birthday = .noBirthday } } } diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Views/SignInView.swift b/Samples/Swift/DaysUntilBirthday/Shared/Views/SignInView.swift index 5eecffcc..4adff36c 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Views/SignInView.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Views/SignInView.swift @@ -19,7 +19,7 @@ import GoogleSignInSwift struct SignInView: View { @EnvironmentObject var authViewModel: AuthenticationViewModel - @ObservedObject var vm = GoogleSignInButtonViewModel() + @StateObject var vm = GoogleSignInButtonViewModel() var body: some View { VStack { diff --git a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift index 93366f47..d4d490cf 100644 --- a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift @@ -37,25 +37,45 @@ struct UserProfileView: View { Text(userProfile.email) } } - NavigationLink(NSLocalizedString("View Days Until Birthday", comment: "View birthday days"), - destination: BirthdayView(birthdayViewModel: birthdayViewModel).onAppear { - guard self.birthdayViewModel.birthday != nil else { - if !self.authViewModel.hasBirthdayReadScope { - self.authViewModel.addBirthdayReadScope { - self.birthdayViewModel.fetchBirthday() + NavigationLink( + NSLocalizedString("View Days Until Birthday", comment: "View birthday days"), + destination: BirthdayView(birthdayViewModel: birthdayViewModel) + .onAppear { + guard self.birthdayViewModel.birthday != nil else { + if !self.authViewModel.hasBirthdayReadScope { + guard let viewController = UIApplication.shared.windows.first?.rootViewController else { + print("There was no root view controller") + return + } + Task { @MainActor in + do { + let signInResult = try await authViewModel.addBirthdayReadScope( + viewController: viewController + ) + self.authViewModel.state = .signedIn(signInResult.user) + self.birthdayViewModel.fetchBirthday() + } catch { + print("Failed to fetch birthday: \(error)") + } + } + } else { + self.birthdayViewModel.fetchBirthday() + } + return } - } else { - self.birthdayViewModel.fetchBirthday() - } - return - } - }) + }) Spacer() } .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { - Button(NSLocalizedString("Disconnect", comment: "Disconnect button"), action: disconnect) - Button(NSLocalizedString("Sign Out", comment: "Sign out button"), action: signOut) + Button( + NSLocalizedString("Disconnect", comment: "Disconnect button"), + action: authViewModel.disconnect + ) + Button( + NSLocalizedString("Sign Out", comment: "Sign out button"), + action: authViewModel.signOut + ) } } } else { @@ -63,12 +83,4 @@ struct UserProfileView: View { } } } - - func disconnect() { - authViewModel.disconnect() - } - - func signOut() { - authViewModel.signOut() - } } diff --git a/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift index d7faad97..6da47934 100644 --- a/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift @@ -21,32 +21,51 @@ struct UserProfileView: View { Text(userProfile.email) } } - Button(NSLocalizedString("Sign Out", comment: "Sign out button"), action: signOut) - .background(Color.blue) - .foregroundColor(Color.white) - .cornerRadius(5) + Button( + NSLocalizedString("Sign Out", comment: "Sign out button"), + action: authViewModel.signOut + ) + .background(Color.blue) + .foregroundColor(Color.white) + .cornerRadius(5) - Button(NSLocalizedString("Disconnect", comment: "Disconnect button"), action: disconnect) - .background(Color.blue) - .foregroundColor(Color.white) - .cornerRadius(5) + Button( + NSLocalizedString("Disconnect", comment: "Disconnect button"), + action: authViewModel.disconnect + ) + .background(Color.blue) + .foregroundColor(Color.white) + .cornerRadius(5) Spacer() - NavigationLink(NSLocalizedString("View Days Until Birthday", comment: "View birthday days"), - destination: BirthdayView(birthdayViewModel: birthdayViewModel).onAppear { - guard self.birthdayViewModel.birthday != nil else { - if !self.authViewModel.hasBirthdayReadScope { - self.authViewModel.addBirthdayReadScope { - self.birthdayViewModel.fetchBirthday() + NavigationLink( + NSLocalizedString("View Days Until Birthday", comment: "View birthday days"), + destination: BirthdayView(birthdayViewModel: birthdayViewModel) + .onAppear { + guard self.birthdayViewModel.birthday != nil else { + if !self.authViewModel.hasBirthdayReadScope { + guard let window = NSApplication.shared.windows.first else { + print("There was no presenting window") + return + } + Task { @MainActor in + do { + let signInResult = + try await authViewModel.addBirthdayReadScope(window: window) + self.authViewModel.state = .signedIn(signInResult.user) + self.birthdayViewModel.fetchBirthday() + } catch { + print("Failed to fetch birthday: \(error)") + } + } + } else { + self.birthdayViewModel.fetchBirthday() + } + return } - } else { - self.birthdayViewModel.fetchBirthday() - } - return - } - }) - .background(Color.blue) - .foregroundColor(Color.white) - .cornerRadius(5) + }) + .background(Color.blue) + .foregroundColor(Color.white) + .cornerRadius(5) Spacer() } } else { @@ -54,12 +73,4 @@ struct UserProfileView: View { } } } - - func disconnect() { - authViewModel.disconnect() - } - - func signOut() { - authViewModel.signOut() - } }