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 All @@ -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<URLSession, Error>) -> 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<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 {
/// A class for authenticating via Google.
final class GoogleSignInAuthenticator {
private var authViewModel: AuthenticationViewModel

/// Creates an instance of this authenticator.
Expand All @@ -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() {
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,72 @@ 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
}
#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.
func signOut() {
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 {
Expand Down
Loading