Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async and await support to DaysUntilBirthday Swift sample app #191

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -51,62 +51,49 @@ final class BirthdayLoader: ObservableObject {
return URLSession(configuration: configuration)
}()

private func sessionWithFreshToken(completion: @escaping (Result<URLSession, Error>) -> 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`.
/// - parameter completion: A closure passing back the `AnyPublisher<Birthday, Error>`
/// 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<Birthday, Error>) -> 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 {
mdmathias marked this conversation as resolved.
Show resolved Hide resolved
let session = try await sessionWithFreshToken()
guard let request = request else {
throw Error.couldNotCreateURLRequest
}
let birthdayData = try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Data, Swift.Error>) -> 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
final class GoogleSignInAuthenticator {
// TODO: Replace this with your own ID.
#if os(iOS)
private let clientID = "687389107077-8qr6dh8fr4uaja89sdr5ieqb7mep04qv.apps.googleusercontent.com"
Expand All @@ -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() {
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,35 @@ final class AuthenticationViewModel: ObservableObject {

/// Signs the user in.
func signIn() {
petea marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading