Skip to content

Commit

Permalink
Updated hub authentication flow
Browse files Browse the repository at this point in the history
- moved business logic out of the coordinators
- show ProgressHUD instead of custom loading screen
- removed unnecessary login screen
- show navigation bar title for all hub authentication screens
  • Loading branch information
phil1995 committed Nov 14, 2023
1 parent a9405be commit 9921496
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 106 deletions.
56 changes: 27 additions & 29 deletions Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,42 +45,40 @@ class AddHubVaultCoordinator: Coordinator {
}

func start() {
let viewModel = HubAuthenticationViewModel(vaultConfig: downloadedVaultConfig.vaultConfig,
hubUserAuthenticator: self,
delegate: self)
let viewController = HubAuthenticationViewController(viewModel: viewModel)
navigationController.pushViewController(viewController, animated: true)
let unlockHandler = AddHubVaultUnlockHandler(vaultUID: vaultUID,
accountUID: accountUID, vaultItem: vaultItem,
downloadedVaultConfig: downloadedVaultConfig,
vaultManager: vaultManager,
delegate: self)
let child = HubAuthenticationCoordinator(navigationController: navigationController,
vaultConfig: downloadedVaultConfig.vaultConfig,
hubAuthenticator: hubAuthenticator,
unlockHandler: unlockHandler,
parent: self,
delegate: self)
childCoordinators.append(child)
child.start()
}
}

extension AddHubVaultCoordinator: HubAuthenticationFlowDelegate {
func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async {
let jwe = response.jwe
let privateKey = response.privateKey
let hubVault = ExistingHubVault(vaultUID: vaultUID,
delegateAccountUID: accountUID,
jweData: jwe.compactSerializedData,
privateKey: privateKey,
vaultItem: vaultItem,
downloadedVaultConfig: downloadedVaultConfig)
do {
try await vaultManager.addExistingHubVault(hubVault).getValue()
childDidFinish(self)
await showSuccessfullyAddedVault()
} catch {
DDLogError("Add existing Hub vault failed: \(error)")
handleError(error, for: navigationController)
}
extension AddHubVaultCoordinator: HubVaultUnlockHandlerDelegate {
func successfullyProcessedUnlockedVault() {
delegate?.showSuccessfullyAddedVault(withName: vaultItem.name, vaultUID: vaultUID)
}

@MainActor
private func showSuccessfullyAddedVault() {
delegate?.showSuccessfullyAddedVault(withName: vaultItem.name, vaultUID: vaultUID)
func failedToProcessUnlockedVault(error: Error) {
handleError(error, for: navigationController, onOKTapped: { [weak self] in
self?.parentCoordinator?.childDidFinish(self)
})
}
}

extension AddHubVaultCoordinator: HubUserLogin {
public func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState {
try await hubAuthenticator.authenticate(with: hubConfig, from: navigationController)
extension AddHubVaultCoordinator: HubAuthenticationCoordinatorDelegate {
func userDidCancelHubAuthentication() {
// do nothing as the user already sees the login screen again
}

func userDismissedHubAuthenticationErrorMessage() {
// do nothing as the user already sees the login screen again
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import AppAuthCore
import CryptomatorCloudAccessCore
import SwiftUI
import UIKit

public protocol HubAuthenticationCoordinatorDelegate: AnyObject {
@MainActor
func userDidCancelHubAuthentication()

@MainActor
func userDismissedHubAuthenticationErrorMessage()
}

public final class HubAuthenticationCoordinator: Coordinator {
public var childCoordinators = [Coordinator]()
public var navigationController: UINavigationController
public weak var parent: Coordinator?

private let vaultConfig: UnverifiedVaultConfig
private let hubAuthenticator: HubAuthenticating
private var progressHUD: ProgressHUD?
private let unlockHandler: HubVaultUnlockHandler
private weak var delegate: HubAuthenticationCoordinatorDelegate?

public init(navigationController: UINavigationController,
vaultConfig: UnverifiedVaultConfig,
hubAuthenticator: HubAuthenticating,
unlockHandler: HubVaultUnlockHandler,
parent: Coordinator?,
delegate: HubAuthenticationCoordinatorDelegate) {
self.navigationController = navigationController
self.vaultConfig = vaultConfig
self.hubAuthenticator = hubAuthenticator
self.unlockHandler = unlockHandler
self.parent = parent
self.delegate = delegate
}

public func start() {
guard let hubConfig = vaultConfig.allegedHubConfig else {
handleError(HubAuthenticationViewModelError.missingHubConfig, for: navigationController, onOKTapped: { [weak self] in
guard let self else { return }
parent?.childDidFinish(self)
})
return
}
Task { @MainActor in
let authenticator = HubUserAuthenticator(hubAuthenticator: hubAuthenticator, viewController: navigationController)
let authState: OIDAuthState
do {
authState = try await authenticator.authenticate(with: hubConfig)
} catch let error as NSError where error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue {
// do not show alert if user canceled it on purpose
delegate?.userDidCancelHubAuthentication()
parent?.childDidFinish(self)
return
} catch {
handleError(error, for: navigationController, onOKTapped: { [weak self] in
guard let self else { return }
delegate?.userDismissedHubAuthenticationErrorMessage()
parent?.childDidFinish(self)
})
return
}
let viewModel = HubAuthenticationViewModel(authState: authState,
vaultConfig: vaultConfig,
unlockHandler: unlockHandler,
delegate: self)
await viewModel.continueToAccessCheck()
guard !viewModel.isLoggedIn else {
// Do not show the authentication view if the user already authenticated successfully
return
}
navigationController.setNavigationBarHidden(false, animated: false)
let viewController = HubAuthenticationViewController(viewModel: viewModel)
navigationController.pushViewController(viewController, animated: true)
}
}

private func showProgressHUD() {
assert(progressHUD == nil, "showProgressHUD called although one is already shown")
progressHUD = ProgressHUD()
progressHUD?.show(presentingViewController: navigationController)
progressHUD?.showLoadingIndicator()
}

private func hideProgressHUD() async {
await withCheckedContinuation { continuation in
progressHUD?.dismiss(animated: true, completion: { [weak self] in
continuation.resume()
self?.progressHUD = nil
})
}
}
}

extension HubAuthenticationCoordinator: HubAuthenticationViewModelDelegate {
public func hubAuthenticationViewModelWantsToShowLoadingIndicator() {
showProgressHUD()
}

public func hubAuthenticationViewModelWantsToHideLoadingIndicator() async {
await hideProgressHUD()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,12 @@ public struct HubAuthenticationView: View {
)
case .accessNotGranted:
HubAccessNotGrantedView(onRefresh: { Task { await viewModel.refresh() }})
case .loading:
ProgressView()
Text(LocalizedString.getValue("hubAuthentication.loading"))
case .userLogin:
HubLoginView(onLogin: { Task { await viewModel.login() }})
case .licenseExceeded:
CryptomatorErrorView(text: LocalizedString.getValue("hubAuthentication.licenseExceeded"))
case let .error(description):
CryptomatorErrorView(text: description)
case .none:
EmptyView()
}
}
.padding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,9 @@ public class HubAuthenticationViewController: UIViewController {

override public func viewDidLoad() {
super.viewDidLoad()
title = LocalizedString.getValue("hubAuthentication.title")

viewModel.$authenticationFlowState
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] in
self?.updateToolbar(state: $0)
})
.store(in: &cancellables)
setupToolBar()
setupSwiftUIView()
}

Expand All @@ -43,6 +39,20 @@ public class HubAuthenticationViewController: UIViewController {
NSLayoutConstraint.activate(child.view.constraints(equalTo: view))
}

private func setupToolBar() {
if let initialState = viewModel.authenticationFlowState {
updateToolbar(state: initialState)
}

viewModel.$authenticationFlowState
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] in
self?.updateToolbar(state: $0)
})
.store(in: &cancellables)
}

/**
Updates the `UINavigationItem` based on the given `state`.
- Note: This solution is far from ideal as we need to update the content of the tool bar in two places, i.e. in this method and inside the SwiftUI itself. Otherwise the behavior can differ when used inside a UINavigationController and a "SwiftUI native" `NavigationView`/ `NavigationStackView`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ public enum HubAuthenticationViewModelError: Error {
case unexpectedSubscriptionHeader
}

public class HubAuthenticationViewModel: ObservableObject {
public protocol HubAuthenticationViewModelDelegate: AnyObject {
@MainActor
func hubAuthenticationViewModelWantsToShowLoadingIndicator()

@MainActor
func hubAuthenticationViewModelWantsToHideLoadingIndicator() async
}

public final class HubAuthenticationViewModel: ObservableObject {
public enum State: Equatable {
case userLogin
case accessNotGranted
case licenseExceeded
case deviceRegistration(DeviceRegistration)
case loading
case error(description: String)
}

Expand All @@ -32,53 +38,37 @@ public class HubAuthenticationViewModel: ObservableObject {
static var subscriptionState: String { "hub-subscription-state" }
}

@Published var authenticationFlowState: State = .userLogin
@Published var authenticationFlowState: State?
@Published public var deviceName: String = UIDevice.current.name
private(set) var isLoggedIn = false

private let vaultConfig: UnverifiedVaultConfig
private let deviceRegisteringService: HubDeviceRegistering
private let hubKeyService: HubKeyReceiving
private let hubUserAuthenticator: HubUserLogin

private var authState: OIDAuthState?
private weak var delegate: HubAuthenticationFlowDelegate?
private let authState: OIDAuthState
private let unlockHandler: HubVaultUnlockHandler
private weak var delegate: HubAuthenticationViewModelDelegate?

public init(vaultConfig: UnverifiedVaultConfig,
public init(authState: OIDAuthState,
vaultConfig: UnverifiedVaultConfig,
deviceRegisteringService: HubDeviceRegistering = CryptomatorHubAuthenticator.shared,
hubUserAuthenticator: HubUserLogin,
hubKeyService: HubKeyReceiving = CryptomatorHubAuthenticator.shared,
delegate: HubAuthenticationFlowDelegate?) {
unlockHandler: HubVaultUnlockHandler,
delegate: HubAuthenticationViewModelDelegate) {
self.authState = authState
self.vaultConfig = vaultConfig
self.deviceRegisteringService = deviceRegisteringService
self.hubUserAuthenticator = hubUserAuthenticator
self.hubKeyService = hubKeyService
self.unlockHandler = unlockHandler
self.delegate = delegate
}

public func login() async {
guard let hubConfig = vaultConfig.allegedHubConfig else {
await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig)
return
}
do {
authState = try await hubUserAuthenticator.authenticate(with: hubConfig)
await continueToAccessCheck()
} catch let error as NSError where error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue {
// ignore user cancellation
} catch {
await setStateToErrorState(with: error)
}
}

public func register() async {
guard let hubConfig = vaultConfig.allegedHubConfig else {
await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig)
return
}
guard let authState = authState else {
await setStateToErrorState(with: HubAuthenticationViewModelError.missingAuthState)
return
}

do {
try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState)
Expand All @@ -94,11 +84,7 @@ public class HubAuthenticationViewModel: ObservableObject {
}

public func continueToAccessCheck() async {
guard let authState = authState else {
await setStateToErrorState(with: HubAuthenticationViewModelError.missingAuthState)
return
}
await setState(to: .loading)
await delegate?.hubAuthenticationViewModelWantsToShowLoadingIndicator()

let authFlow: HubAuthenticationFlow
do {
Expand All @@ -107,6 +93,8 @@ public class HubAuthenticationViewModel: ObservableObject {
await setStateToErrorState(with: error)
return
}
await delegate?.hubAuthenticationViewModelWantsToHideLoadingIndicator()

switch authFlow {
case let .success(data, header):
await receivedExistingKey(data: data, header: header)
Expand Down Expand Up @@ -134,7 +122,8 @@ public class HubAuthenticationViewModel: ObservableObject {
let response = HubUnlockResponse(jwe: jwe,
privateKey: privateKey,
subscriptionState: subscriptionState)
await delegate?.didSuccessfullyRemoteUnlock(response)
await MainActor.run { isLoggedIn = true }
await unlockHandler.didSuccessfullyRemoteUnlock(response)
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import AppAuthCore
import CryptomatorCloudAccessCore
import UIKit

struct HubUserAuthenticator: HubUserLogin {
private let hubAuthenticator: HubAuthenticating
private let viewController: UIViewController

init(hubAuthenticator: HubAuthenticating, viewController: UIViewController) {
self.hubAuthenticator = hubAuthenticator
self.viewController = viewController
}

func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState {
try await hubAuthenticator.authenticate(with: hubConfig, from: viewController)
}
}
Loading

0 comments on commit 9921496

Please sign in to comment.