diff --git a/GoogleSignIn/Sources/GIDAuthentication.m b/GoogleSignIn/Sources/GIDAuthentication.m index ed4f5154..ff1a8183 100644 --- a/GoogleSignIn/Sources/GIDAuthentication.m +++ b/GoogleSignIn/Sources/GIDAuthentication.m @@ -219,7 +219,7 @@ - (NSString *)emmSupport { return authorization; } -- (void)doWithFreshTokens:(GIDAuthenticationCompletion)completion { +- (void)doWithFreshTokens:(GIDAuthenticationAction)completion { if (!([self.accessTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire || (self.idToken && [self.idTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire))) { dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj index 329cad53..9a50e97d 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj @@ -656,6 +656,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"macOS/Preview Content\""; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = macOS/Info.plist; @@ -685,6 +686,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"macOS/Preview Content\""; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = macOS/Info.plist; diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift index 1dc4dea4..2bbea55b 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" @@ -51,20 +51,18 @@ final class BirthdayLoader: ObservableObject { return URLSession(configuration: configuration) }() - private func sessionWithFreshToken(completion: @escaping (Result) -> Void) { - let authentication = GIDSignIn.sharedInstance.currentUser?.authentication - authentication?.do { auth, error in - guard let token = auth?.accessToken else { - completion(.failure(.couldNotCreateURLSession(error))) - return - } - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = [ - "Authorization": "Bearer \(token)" - ] - let session = URLSession(configuration: configuration) - completion(.success(session)) + private func sessionWithFreshToken() async throws -> URLSession { + guard let authentication = GIDSignIn.sharedInstance.currentUser?.authentication else { + throw Error.noCurrentUserForSessionWithFreshToken } + + let freshAuth = try await authentication.doWithFreshTokens() + let configuration = URLSessionConfiguration.default + configuration.httpAdditionalHeaders = [ + "Authorization": "Bearer \(freshAuth.accessToken)" + ] + let session = URLSession(configuration: configuration) + return session } /// Creates a `Publisher` to fetch a user's `Birthday`. @@ -72,41 +70,32 @@ final class BirthdayLoader: ObservableObject { /// 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()) + 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 2a0e88fd..4a067eca 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -16,6 +16,11 @@ 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 { @@ -38,40 +43,32 @@ 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(with: configuration, - presenting: rootViewController) { user, error in - guard let user = user else { - print("Error! \(String(describing: error))") - return - } - self.authViewModel.state = .signedIn(user) - } -#elseif os(macOS) - guard let presentingWindow = NSApplication.shared.windows.first else { - print("There is no presenting window!") - return - } + #if os(iOS) + /// Signs in the user based upon the selected account. + /// - parameter rootViewController: The `UIViewController` to use during the sign in flow. + /// - returns: The signed in `GIDGoogleUser`. + /// - throws: Any error that may arise during the sign in process. + func signIn(with rootViewController: UIViewController) async throws -> GIDGoogleUser { + return try await GIDSignIn.sharedInstance.signIn( + with: configuration, + presenting: rootViewController + ) + } + #endif - GIDSignIn.sharedInstance.signIn(with: configuration, - presenting: presentingWindow) { user, error in - guard let user = user else { - print("Error! \(String(describing: error))") - return - } - self.authViewModel.state = .signedIn(user) - } -#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 signed in `GIDGoogleUser`. + /// - throws: Any error that may arise during the sign in process. + func signIn(with window: NSWindow) async throws -> GIDGoogleUser { + return try await GIDSignIn.sharedInstance.signIn( + with: configuration, + presenting: window + ) } + #endif /// Signs out the current user. func signOut() { @@ -80,57 +77,41 @@ 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() - } + func disconnect() async throws { + try await GIDSignIn.sharedInstance.disconnect() } - // 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) { - #if os(iOS) - guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else { - fatalError("No root view controller!") - } - - GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope], - presenting: rootViewController) { user, error in - if let error = error { - print("Found error while adding birthday read scope: \(error).") - return - } - - guard let currentUser = user else { return } - self.authViewModel.state = .signedIn(currentUser) - completion() - } - - #elseif os(macOS) - guard let presentingWindow = NSApplication.shared.windows.first else { - fatalError("No presenting window!") - } - - GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope], - presenting: presentingWindow) { user, error in - if let error = error { - print("Found error while adding birthday read scope: \(error).") - return - } - - guard let currentUser = user else { return } - self.authViewModel.state = .signedIn(currentUser) - completion() - } + /// - parameter viewController: The `UIViewController` to use while authorizing the scope. + /// - returns: The `GIDGoogleUser` with the authorized scope. + /// - throws: Any error that may arise while authorizing the scope. + func addBirthdayReadScope(viewController: UIViewController) async throws -> GIDGoogleUser { + return try await GIDSignIn.sharedInstance.addScopes( + [BirthdayLoader.birthdayReadScope], + presenting: viewController + ) + } +#endif - #endif +#if os(macOS) + /// Adds the birthday read scope for the current user. + /// - parameter window: The `NSWindow` to use while authorizing the scope. + /// - returns: The `GIDGoogleUser` with the authorized scope. + /// - throws: Any error that may arise while authorizing the scope. + func addBirthdayReadScope(window: NSWindow) async throws -> GIDGoogleUser { + return try await GIDSignIn.sharedInstance.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..b5d1bbb3 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift @@ -47,7 +47,35 @@ 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 + } + + Task { @MainActor in + do { + let user = try await authenticator.signIn(with: rootViewController) + self.state = .signedIn(user) + } catch { + print("Error signing in: \(error)") + } + } +#elseif os(macOS) + guard let presentingWindow = NSApplication.shared.windows.first else { + print("There is no presenting window!") + return + } + + Task { @MainActor in + do { + let user = try await authenticator.signIn(with: presentingWindow) + self.state = .signedIn(user) + } catch { + print("Error signing in: \(error)") + } + } +#endif } /// Signs the user out. @@ -57,19 +85,39 @@ final class AuthenticationViewModel: ObservableObject { /// Disconnects the previously granted scope and logs the user out. func disconnect() { - authenticator.disconnect() + Task { @MainActor in + do { + try await authenticator.disconnect() + authenticator.signOut() + } 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: A `GIDGoogleUser` with the authorized scope. + /// - throws: Any error that may arise while adding the read birthday scope. + func addBirthdayReadScope(viewController: UIViewController) async throws -> GIDGoogleUser { + 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: A `GIDGoogleUser` with the authorized scope. + /// - throws: Any error that may arise while adding the read birthday scope. + func addBirthdayReadScope(window: NSWindow) async throws -> GIDGoogleUser { + 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/iOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift index 93366f47..c8339e1a 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 user = try await authViewModel.addBirthdayReadScope( + viewController: viewController + ) + self.authViewModel.state = .signedIn(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/DaysUntilBirthdayOnMac.entitlements b/Samples/Swift/DaysUntilBirthday/macOS/DaysUntilBirthdayOnMac.entitlements index 625af03d..5776a3a2 100644 --- a/Samples/Swift/DaysUntilBirthday/macOS/DaysUntilBirthdayOnMac.entitlements +++ b/Samples/Swift/DaysUntilBirthday/macOS/DaysUntilBirthdayOnMac.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.client + keychain-access-groups + diff --git a/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift index d7faad97..5faf1538 100644 --- a/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift @@ -21,32 +21,50 @@ 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 user = try await authViewModel.addBirthdayReadScope(window: window) + self.authViewModel.state = .signedIn(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 +72,4 @@ struct UserProfileView: View { } } } - - func disconnect() { - authViewModel.disconnect() - } - - func signOut() { - authViewModel.signOut() - } }