Add a state machine to the AuthenticationFlowCoordinator. (#4103)

This commit is contained in:
Doug
2025-05-08 15:41:52 +01:00
committed by GitHub
parent 6f1a661747
commit 21b4f5805b
6 changed files with 221 additions and 39 deletions

View File

@@ -6,6 +6,7 @@
//
import Combine
import SwiftState
import SwiftUI
@MainActor
@@ -24,6 +25,72 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private let userIndicatorController: UserIndicatorControllerProtocol
private let qrCodeLoginService: QRCodeLoginServiceProtocol
enum State: StateType {
/// The state machine hasn't started.
case initial
/// The initial screen shown when you first launch the app.
case startScreen
/// The screen used for the whole QR Code flow.
case qrCodeLoginScreen
/// The screen to continue authentication with the current server.
case serverConfirmationScreen
/// The screen to choose a different server.
case serverSelectionScreen
/// The web authentication session is being presented.
case oidcAuthentication
/// The screen to login with a password.
case loginScreen
/// The screen to report an error.
case bugReportFlow
/// The flow is complete.
case complete
}
enum Event: EventType {
/// The flow is being started.
case start
/// The user would like to login with a QR code.
case loginWithQR
/// The user would like to login without a QR code.
case loginManually
/// The user would like to register a new account.
case register
/// The user encountered a problem.
case reportProblem
/// The QR login flow was aborted.
case cancelledLoginWithQR
/// The user aborted manual login.
case cancelledServerConfirmation
/// The user would like to enter a different server.
case changeServer(AuthenticationFlow)
/// The user is no longer selecting a server.
case dismissedServerSelection
/// Show the web authentication session for OIDC.
case continueWithOIDC
/// The web authentication session was aborted.
case cancelledOIDCAuthentication
/// Show the screen to login with password.
case continueWithPassword
/// The password login was aborted.
case cancelledPasswordLogin
/// The user has finished reporting a problem (or viewing the logs).
case bugReportFlowComplete
/// The user has successfully signed in!
case signedIn
}
private let stateMachine: StateMachine<State, Event>
private var cancellables = Set<AnyCancellable>()
private var oidcPresenter: OIDCAuthenticationPresenter?
@@ -51,10 +118,13 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
self.qrCodeLoginService = qrCodeLoginService
navigationStackCoordinator = NavigationStackCoordinator()
stateMachine = .init(state: .initial)
configureStateMachine()
}
func start() {
showStartScreen()
stateMachine.tryEvent(.start)
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
@@ -65,7 +135,78 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
fatalError()
}
// MARK: - Private
// MARK: - Setup
private func configureStateMachine() {
stateMachine.addRoutes(event: .start, transitions: [.initial => .startScreen]) { [weak self] _ in
self?.showStartScreen()
}
// QR Code
stateMachine.addRoutes(event: .loginWithQR, transitions: [.startScreen => .qrCodeLoginScreen]) { [weak self] _ in
self?.showQRCodeLoginScreen()
}
stateMachine.addRoutes(event: .cancelledLoginWithQR, transitions: [.qrCodeLoginScreen => .startScreen])
// Manual Authentication
stateMachine.addRoutes(event: .loginManually, transitions: [.startScreen => .serverConfirmationScreen]) { [weak self] _ in
self?.showServerConfirmationScreen(authenticationFlow: .login)
}
stateMachine.addRoutes(event: .register, transitions: [.startScreen => .serverConfirmationScreen]) { [weak self] _ in
self?.showServerConfirmationScreen(authenticationFlow: .register)
}
stateMachine.addRoutes(event: .cancelledServerConfirmation, transitions: [.serverConfirmationScreen => .startScreen])
stateMachine.addRoutes(event: .changeServer(.login), transitions: [.serverConfirmationScreen => .serverSelectionScreen]) { [weak self] _ in
self?.showServerSelectionScreen(authenticationFlow: .login)
}
stateMachine.addRoutes(event: .changeServer(.register), transitions: [.serverConfirmationScreen => .serverSelectionScreen]) { [weak self] _ in
self?.showServerSelectionScreen(authenticationFlow: .register)
}
stateMachine.addRoutes(event: .dismissedServerSelection, transitions: [.serverSelectionScreen => .serverConfirmationScreen])
stateMachine.addRoutes(event: .continueWithOIDC, transitions: [.serverConfirmationScreen => .oidcAuthentication]) { [weak self] context in
guard let (oidcData, window) = context.userInfo as? (OIDCAuthorizationDataProxy, UIWindow) else {
fatalError("Missing the OIDC data and presentation anchor.")
}
self?.showOIDCAuthentication(oidcData: oidcData, presentationAnchor: window)
}
stateMachine.addRoutes(event: .cancelledOIDCAuthentication, transitions: [.oidcAuthentication => .serverConfirmationScreen])
stateMachine.addRoutes(event: .continueWithPassword, transitions: [.serverConfirmationScreen => .loginScreen]) { [weak self] _ in
self?.showLoginScreen()
}
stateMachine.addRoutes(event: .cancelledPasswordLogin, transitions: [.loginScreen => .serverConfirmationScreen])
// Bug Report
stateMachine.addRoutes(event: .reportProblem, transitions: [.startScreen => .bugReportFlow]) { [weak self] _ in
self?.startBugReportFlow()
}
stateMachine.addRoutes(event: .bugReportFlowComplete, transitions: [.bugReportFlow => .startScreen])
// Completion
stateMachine.addRoutes(event: .signedIn, transitions: [.qrCodeLoginScreen => .complete,
.oidcAuthentication => .complete,
.loginScreen => .complete]) { [weak self] context in
guard let userSession = context.userInfo as? UserSessionProtocol else { fatalError("The user session wasn't included in the context") }
self?.userHasSignedIn(userSession: userSession)
}
// Unhandled
stateMachine.addErrorHandler { context in
switch (context.fromState, context.toState) {
case (.complete, .complete):
break // Ignore all events triggered by
default:
fatalError("Unexpected transition: \(context)")
}
}
}
private func showStartScreen() {
let parameters = AuthenticationStartScreenParameters(showCreateAccountButton: appSettings.showCreateAccountButton,
@@ -77,14 +218,14 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return }
switch action {
case .loginManually:
showServerConfirmationScreen(authenticationFlow: .login)
case .loginWithQR:
startQRCodeLogin()
stateMachine.tryEvent(.loginWithQR)
case .loginManually:
stateMachine.tryEvent(.loginManually)
case .register:
showServerConfirmationScreen(authenticationFlow: .register)
stateMachine.tryEvent(.register)
case .reportProblem:
showReportProblemScreen()
stateMachine.tryEvent(.reportProblem)
}
}
.store(in: &cancellables)
@@ -94,7 +235,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
navigationRootCoordinator.setRootCoordinator(navigationStackCoordinator)
}
private func startQRCodeLogin() {
// MARK: - QR Code
private func showQRCodeLoginScreen() {
let coordinator = QRCodeLoginScreenCoordinator(parameters: .init(qrCodeLoginService: qrCodeLoginService,
orientationManager: appMediator.windowManager,
appMediator: appMediator))
@@ -105,29 +248,25 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .signInManually:
navigationStackCoordinator.setSheetCoordinator(nil)
showServerConfirmationScreen(authenticationFlow: .login)
stateMachine.tryEvent(.cancelledLoginWithQR)
stateMachine.tryEvent(.loginManually)
case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.cancelledLoginWithQR)
case .done(let userSession):
navigationStackCoordinator.setSheetCoordinator(nil)
// Since the qr code login flow includes verification
appSettings.hasRunIdentityConfirmationOnboarding = true
DispatchQueue.main.async {
self.userHasSignedIn(userSession: userSession)
self.stateMachine.tryEvent(.signedIn, userInfo: userSession)
}
}
}
.store(in: &cancellables)
navigationStackCoordinator.setSheetCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(coordinator) // Don't use the callback (interactive dismiss disabled), choose the event with the action.
}
private func showReportProblemScreen() {
bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(navigationStackCoordinator),
userIndicatorController: userIndicatorController,
bugReportService: bugReportService,
userSession: nil))
bugReportFlowCoordinator?.start()
}
// MARK: - Manual Authentication
private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) {
// Reset the service back to the default homeserver before continuing. This ensures
@@ -144,16 +283,18 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .continueWithOIDC(let oidcData, let window):
showOIDCAuthentication(oidcData: oidcData, presentationAnchor: window)
stateMachine.tryEvent(.continueWithOIDC, userInfo: (oidcData, window))
case .continueWithPassword:
showLoginScreen()
stateMachine.tryEvent(.continueWithPassword)
case .changeServer:
showServerSelectionScreen(authenticationFlow: authenticationFlow)
stateMachine.tryEvent(.changeServer(authenticationFlow))
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.cancelledServerConfirmation)
}
}
private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow) {
@@ -178,7 +319,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
.store(in: &cancellables)
navigationCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissedServerSelection)
}
}
private func showOIDCAuthentication(oidcData: OIDCAuthorizationDataProxy, presentationAnchor: UIWindow) {
@@ -191,9 +334,10 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
Task {
switch await presenter.authenticate(using: oidcData) {
case .success(let userSession):
userHasSignedIn(userSession: userSession)
stateMachine.tryEvent(.signedIn, userInfo: userSession)
case .failure:
break // Nothing to do, the alerts are handled by the presenter.
stateMachine.tryEvent(.cancelledOIDCAuthentication)
// Nothing more to do, the alerts are handled by the presenter.
}
oidcPresenter = nil
}
@@ -211,7 +355,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .signedIn(let userSession):
userHasSignedIn(userSession: userSession)
stateMachine.tryEvent(.signedIn, userInfo: userSession)
case .configuredForOIDC:
// Pop back to the confirmation screen for OIDC login to continue.
navigationStackCoordinator.pop(animated: false)
@@ -219,8 +363,31 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.cancelledPasswordLogin)
}
}
// MARK: - Bug Report
private func startBugReportFlow() {
let coordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(navigationStackCoordinator),
userIndicatorController: userIndicatorController,
bugReportService: bugReportService,
userSession: nil))
coordinator.actionsPublisher.sink { [weak self] action in
switch action {
case .complete:
self?.stateMachine.tryEvent(.bugReportFlowComplete)
}
}
.store(in: &cancellables)
bugReportFlowCoordinator = coordinator
coordinator.start()
}
// MARK: - Completion
private func userHasSignedIn(userSession: UserSessionProtocol) {
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)

View File

@@ -8,6 +8,11 @@
import Combine
import Foundation
enum BugReportFlowCoordinatorAction: Equatable {
/// The flow is complete.
case complete
}
struct BugReportFlowCoordinatorParameters {
enum PresentationMode {
case sheet(NavigationStackCoordinator)
@@ -24,6 +29,11 @@ class BugReportFlowCoordinator: FlowCoordinatorProtocol {
private let parameters: BugReportFlowCoordinatorParameters
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<BugReportFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<BugReportFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
private var internalNavigationStackCoordinator: NavigationStackCoordinator?
private var isModallyPresented: Bool {
@@ -79,11 +89,15 @@ class BugReportFlowCoordinator: FlowCoordinatorProtocol {
case .sheet(let navigationStackCoordinator):
let internalNavigationStackCoordinator = NavigationStackCoordinator()
internalNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(internalNavigationStackCoordinator)
navigationStackCoordinator.setSheetCoordinator(internalNavigationStackCoordinator) { [weak self] in
self?.actionsSubject.send(.complete)
}
self.internalNavigationStackCoordinator = internalNavigationStackCoordinator
case .push(let navigationStackCoordinator):
internalNavigationStackCoordinator = navigationStackCoordinator
navigationStackCoordinator.push(coordinator)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.actionsSubject.send(.complete)
}
}
}

View File

@@ -27,9 +27,6 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
private let userIndicatorController: UserIndicatorControllerProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
// periphery:ignore - retaining purpose
private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator?
enum State: StateType {
/// The state machine hasn't started.
case initial

View File

@@ -10,15 +10,15 @@ import SwiftUI
// MARK: - Coordinator
enum AuthenticationStartScreenCoordinatorAction {
case loginManually
case loginWithQR
case loginManually
case register
case reportProblem
}
enum AuthenticationStartScreenViewModelAction {
case loginManually
case loginWithQR
case loginManually
case register
case reportProblem
}
@@ -30,8 +30,8 @@ struct AuthenticationStartScreenViewState: BindableState {
}
enum AuthenticationStartScreenViewAction {
case loginManually
case loginWithQR
case loginManually
case register
case reportProblem
}

View File

@@ -25,10 +25,10 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
override func process(viewAction: AuthenticationStartScreenViewAction) {
switch viewAction {
case .loginManually:
actionsSubject.send(.loginManually)
case .loginWithQR:
actionsSubject.send(.loginWithQR)
case .loginManually:
actionsSubject.send(.loginManually)
case .register:
actionsSubject.send(.register)
case .reportProblem:

View File

@@ -66,7 +66,7 @@ enum OIDCError: Error {
case unknown
}
struct OIDCAuthorizationDataProxy: Equatable {
struct OIDCAuthorizationDataProxy: Hashable {
let underlyingData: OAuthAuthorizationData
var url: URL {
@@ -77,8 +77,12 @@ struct OIDCAuthorizationDataProxy: Equatable {
}
}
extension OAuthAuthorizationData: @retroactive Equatable {
extension OAuthAuthorizationData: @retroactive Hashable {
public static func == (lhs: MatrixRustSDK.OAuthAuthorizationData, rhs: MatrixRustSDK.OAuthAuthorizationData) -> Bool {
lhs.loginUrl() == rhs.loginUrl()
}
public func hash(into hasher: inout Hasher) {
hasher.combine(loginUrl())
}
}