diff --git a/apple/OmnivoreKit/Package.swift b/apple/OmnivoreKit/Package.swift index cafef369a..f5ef78249 100644 --- a/apple/OmnivoreKit/Package.swift +++ b/apple/OmnivoreKit/Package.swift @@ -33,7 +33,6 @@ let package = Package( name: "Services", dependencies: [ .product(name: "GoogleSignIn", package: "GoogleSignIn-iOS"), - .product(name: "AppAuth", package: "AppAuth-iOS"), "Valet", .product(name: "SwiftGraphQL", package: "swift-graphql"), "Models", @@ -64,7 +63,6 @@ var appPackageDependencies: [Target.Dependency] { var dependencies: [Package.Dependency] { var deps: [Package.Dependency] = [ - .package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.4.0")), .package(url: "https://github.com/Square/Valet", from: "4.1.2"), .package(url: "https://github.com/maticzav/swift-graphql", from: "2.3.1"), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift index f7d5a496c..5d71bf922 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift @@ -66,21 +66,18 @@ final class RegistrationViewModel: ObservableObject { .store(in: &subscriptions) } - func handleGoogleAuth(authenticator: Authenticator) { - authenticator - .handleGoogleAuth(presentingViewController: presentingViewController()) - .sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(loginError) = completion else { return } - self?.loginError = loginError - }, - receiveValue: { [weak self] isNewAccount in - if isNewAccount { - self?.registrationState = .createProfile(userProfile: UserProfile(username: "", name: "")) - } - } - ) - .store(in: &subscriptions) + func handleGoogleAuth(authenticator: Authenticator) async { + guard let presentingViewController = presentingViewController() else { return } + let googleAuthResponse = await authenticator.handleGoogleAuth(presenting: presentingViewController) + + switch googleAuthResponse { + case let .loginError(error): + loginError = error + case .newOmnivoreUser: + registrationState = .createProfile(userProfile: UserProfile(username: "", name: "")) + case .existingOmnivoreUser: + break + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index a946c7eb7..77f6c2982 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -97,6 +97,7 @@ struct InnerRootView: View { } } #endif + .onOpenURL { Authenticator.handleGoogleURL(url: $0) } } #if os(iOS) diff --git a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift index 4a45a70e3..32509e4e9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift @@ -139,7 +139,9 @@ struct WelcomeView: View { if AppKeys.sharedInstance?.iosClientGoogleId != nil { GoogleAuthButton { - viewModel.handleGoogleAuth(authenticator: authenticator) + Task { + await viewModel.handleGoogleAuth(authenticator: authenticator) + } } } } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index a31c56ba3..354c834ad 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -1,6 +1,6 @@ -import AppAuth import Combine import Foundation +import GoogleSignIn import Models import Utils import WebKit @@ -8,6 +8,10 @@ import WebKit public final class Authenticator: ObservableObject { public static var unregisterIntercomUser: (() -> Void)? + public static func handleGoogleURL(url: URL) { + GIDSignIn.sharedInstance.handle(url) + } + public enum AuthStatus { case loggedOut case pendingUser @@ -20,13 +24,8 @@ public final class Authenticator: ObservableObject { let networker: Networker var subscriptions = Set() - var currentAuthorizationFlow: OIDExternalUserAgentSession? var pendingUserToken: String? - #if os(macOS) - var authRedirectHandler: OIDRedirectHTTPHandler? - #endif - public init(networker: Networker) { self.networker = networker self.isLoggedIn = ValetKey.authToken.exists diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift b/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift index be2429db2..b3eb47192 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift @@ -1,123 +1,144 @@ -import AppAuth import Combine import Foundation +import GoogleSignIn import Models import Utils -#if os(iOS) - import UIKit +public enum GoogleAuthResponse { + case loginError(error: LoginError) + case newOmnivoreUser + case existingOmnivoreUser +} - public extension Authenticator { - func handleGoogleAuth(presentingViewController: PlatformViewController?) -> AnyPublisher { - Future { [weak self] promise in - guard let self = self, let presenting = presentingViewController else { return } - - // swiftlint:disable:next line_length - self.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: self.googleAuthRequest(redirectURL: nil), presenting: presenting) { authState, authError in - self.resolveAuthResponse(promise: promise, authState: authState, authError: authError) - } - } - .eraseToAnyPublisher() - } +extension Authenticator { + public func handleGoogleAuth(presenting: PlatformViewController) async -> GoogleAuthResponse { + let authToken = try? await googleSignIn(presenting: presenting) + guard let authToken = authToken else { return .loginError(error: .unauthorized) } + // TODO: sync with server + return .existingOmnivoreUser } -#endif -#if os(macOS) - import AppKit - - public extension Authenticator { - func handleGoogleAuth(presentingViewController _: PlatformViewController?) -> AnyPublisher { - authRedirectHandler = OIDRedirectHTTPHandler( - successURL: URL(string: "https://omnivore.app")! - ) - let redirectURL = authRedirectHandler?.startHTTPListener(nil) - let authRequest = googleAuthRequest(redirectURL: redirectURL) - - return Future { [weak self] promise in - guard let self = self else { return } - - // swiftlint:disable:next line_length - self.authRedirectHandler?.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: authRequest) { authState, authError in - NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) - self.resolveAuthResponse(promise: promise, authState: authState, authError: authError) + func googleSignIn(presenting: PlatformViewController) async throws -> String { + try await withCheckedThrowingContinuation { continuation in + let clientID = "\(AppKeys.sharedInstance?.iosClientGoogleId ?? "").apps.googleusercontent.com" + GIDSignIn.sharedInstance.signIn( + with: GIDConfiguration(clientID: clientID), + presenting: presenting + ) { user, error in + guard let user = user, error == nil else { + continuation.resume(throwing: LoginError.unauthorized) + return } - } - .eraseToAnyPublisher() - } - } -#endif -private extension Authenticator { - func resolveAuthResponse( - promise: @escaping (Result) -> Void, - authState: OIDAuthState?, - authError: Error? - ) { - if let idToken = authState?.lastTokenResponse?.idToken { - Task { - do { - let authPayload = try await networker.submitGoogleToken(idToken: idToken) - try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString) - try ValetKey.authToken.setValue(authPayload.authToken) - DispatchQueue.main.async { - self.isLoggedIn = true - } - } catch { - if let error = error as? LoginError { - switch error { - case .unauthorized, .unknown: - self.resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError) - case .network: - promise(.failure(error)) - } - self.resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError) + user.authentication.do { authentication, error in + guard let idToken = authentication?.idToken, error == nil else { + continuation.resume(throwing: LoginError.unauthorized) + return } + + continuation.resume(returning: idToken) } } - } else { - resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError) } } - - func resolveAuthResponseForAccountCreation( - promise: @escaping (Result) -> Void, - authState: OIDAuthState?, - authError _: Error? - ) { - if let idToken = authState?.lastTokenResponse?.idToken { - let params = CreatePendingAccountParams(token: idToken, provider: .google, fullName: nil) - let encodedParams = (try? JSONEncoder().encode(params)) ?? Data() - - networker - .createPendingUser(params: encodedParams) - .sink { completion in - guard case let .failure(serverError) = completion else { return } - promise(.failure(LoginError.make(serverError: serverError))) - } receiveValue: { [weak self] in - self?.pendingUserToken = $0.pendingUserToken - promise(.success(true)) - } - .store(in: &subscriptions) - } else { - promise(.failure(.unauthorized)) - } - } - - func googleAuthRequest(redirectURL: URL?) -> OIDAuthorizationRequest { - let authEndpoint = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")! - let tokenEndpoint = URL(string: "https://www.googleapis.com/oauth2/v4/token")! - let iosClientGoogleId = AppKeys.sharedInstance?.iosClientGoogleId ?? "" - let scopes = ["profile", "email"] - - return OIDAuthorizationRequest( - configuration: OIDServiceConfiguration(authorizationEndpoint: authEndpoint, tokenEndpoint: tokenEndpoint), - clientId: "\(iosClientGoogleId).apps.googleusercontent.com", - scopes: scopes, - redirectURL: redirectURL ?? URL( - string: "com.googleusercontent.apps.\(iosClientGoogleId):/oauth2redirect/google" - )!, - responseType: "code", - additionalParameters: nil - ) - } } + +// #if os(iOS) +// import UIKit +// +// public extension Authenticator { +// func handleGoogleAuthDep(presentingViewController: PlatformViewController?) -> AnyPublisher { +// Future { [weak self] promise in +// guard let self = self, let presenting = presentingViewController else { return } +// +// // swiftlint:disable:next line_length +// self.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: self.googleAuthRequest(redirectURL: nil), presenting: presenting) { authState, authError in +// self.resolveAuthResponse(promise: promise, authState: authState, authError: authError) +// } +// } +// .eraseToAnyPublisher() +// } +// } +// #endif + +// #if os(macOS) +// import AppKit +// +// public extension Authenticator { +// func handleGoogleAuthDep(presentingViewController _: PlatformViewController?) -> AnyPublisher { +// authRedirectHandler = OIDRedirectHTTPHandler( +// successURL: URL(string: "https://omnivore.app")! +// ) +// let redirectURL = authRedirectHandler?.startHTTPListener(nil) +// let authRequest = googleAuthRequest(redirectURL: redirectURL) +// +// return Future { [weak self] promise in +// guard let self = self else { return } +// +// // swiftlint:disable:next line_length +// self.authRedirectHandler?.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: authRequest) { authState, authError in +// NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) +// self.resolveAuthResponse(promise: promise, authState: authState, authError: authError) +// } +// } +// .eraseToAnyPublisher() +// } +// } +// #endif + +// private extension Authenticator { +// func resolveAuthResponse( +// promise: @escaping (Result) -> Void, +// authState: OIDAuthState?, +// authError: Error? +// ) { +// if let idToken = authState?.lastTokenResponse?.idToken { +// Task { +// do { +// let authPayload = try await networker.submitGoogleToken(idToken: idToken) +// try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString) +// try ValetKey.authToken.setValue(authPayload.authToken) +// DispatchQueue.main.async { +// self.isLoggedIn = true +// } +// } catch { +// if let error = error as? LoginError { +// switch error { +// case .unauthorized, .unknown: +// self.resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError) +// case .network: +// promise(.failure(error)) +// } +// self.resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError) +// } +// } +// } +// } else { +// resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError) +// } +// } +// +// func resolveAuthResponseForAccountCreation( +// promise: @escaping (Result) -> Void, +// authState: OIDAuthState?, +// authError _: Error? +// ) { +// if let idToken = authState?.lastTokenResponse?.idToken { +// let params = CreatePendingAccountParams(token: idToken, provider: .google, fullName: nil) +// let encodedParams = (try? JSONEncoder().encode(params)) ?? Data() +// +// networker +// .createPendingUser(params: encodedParams) +// .sink { completion in +// guard case let .failure(serverError) = completion else { return } +// promise(.failure(LoginError.make(serverError: serverError))) +// } receiveValue: { [weak self] in +// self?.pendingUserToken = $0.pendingUserToken +// promise(.success(true)) +// } +// .store(in: &subscriptions) +// } else { +// promise(.failure(.unauthorized)) +// } +// } +// }