Combine the Server Selection screen into the Server Confirmation screen and add autocompletion when entering an account provider. (#5949)

* Adjust login flow to exclusively use the ServerSelectionScreen

* Move async code to server selection vm

* ServerSelection supports pro picker options

* Use homeserver address for both confirmation and picker modes

* rename ServerSelectionScreenMode

* remove ServerConfirmationScreen

It's been superseded by the ServerSelectionScreen

* rename userInput

* Update strings with latest design strings

* Add server input autocompletion

* Store previous servers in AppSettings and autofill on server signin

* Audit changes for logging

* Enforce lowercase server history storage

* Update tests

* Update Xcode project

* PR review fixups

* Add HomeserverHistoryManager

* Review fixups 2

* Review fixups 3
This commit is contained in:
Michael
2026-08-10 12:38:37 -05:00
committed by GitHub
parent 42a7b6e8ee
commit 6efa8743ff
71 changed files with 977 additions and 1357 deletions
@@ -21,6 +21,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private let navigationStackCoordinator: NavigationStackCoordinator
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let homeserverHistoryManager: HomeserverHistoryManager
private let appHooks: AppHooks
private let userIndicatorController: UserIndicatorControllerProtocol
@@ -34,10 +35,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
/// 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.
/// The screen used to choose a server
case serverSelectionScreen
/// The screen to login with a password.
case loginScreen
@@ -60,17 +60,12 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
/// The user would like to login with a QR code.
case loginWithQR
/// Show the server confirmation screen.
case confirmServer(AuthenticationFlow)
case selectServer(AuthenticationFlow)
/// 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
case cancelledServerSelection
/// Show the screen to login with password (with the optional login hint in the `userInfo`).
case continueWithPassword
@@ -113,6 +108,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
self.navigationRootCoordinator = navigationRootCoordinator
self.appMediator = appMediator
self.appSettings = appSettings
homeserverHistoryManager = HomeserverHistoryManager(appSettings: appSettings)
self.appHooks = appHooks
self.userIndicatorController = userIndicatorController
@@ -155,10 +151,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
case .qrCodeLoginScreen:
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.cancelledLoginWithQR) // Needs to be handled manually.
case .serverConfirmationScreen:
navigationStackCoordinator.popToRoot(animated: animated)
case .serverSelectionScreen:
navigationStackCoordinator.setSheetCoordinator(nil)
navigationStackCoordinator.popToRoot(animated: animated)
case .loginScreen:
navigationStackCoordinator.popToRoot(animated: animated)
@@ -202,28 +195,20 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
// Manual Authentication
stateMachine.addRoutes(event: .confirmServer(.login), transitions: [.startScreen => .serverConfirmationScreen]) { [weak self] _ in
self?.showServerConfirmationScreen(authenticationFlow: .login)
}
stateMachine.addRoutes(event: .confirmServer(.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
stateMachine.addRoutes(event: .selectServer(.login), transitions: [.startScreen => .serverSelectionScreen]) { [weak self] _ in
self?.showServerSelectionScreen(authenticationFlow: .login)
}
stateMachine.addRoutes(event: .changeServer(.register), transitions: [.serverConfirmationScreen => .serverSelectionScreen]) { [weak self] _ in
stateMachine.addRoutes(event: .selectServer(.register), transitions: [.startScreen => .serverSelectionScreen]) { [weak self] _ in
self?.showServerSelectionScreen(authenticationFlow: .register)
}
stateMachine.addRoutes(event: .dismissedServerSelection, transitions: [.serverSelectionScreen => .serverConfirmationScreen])
stateMachine.addRoutes(event: .cancelledServerSelection, transitions: [.serverSelectionScreen => .startScreen])
stateMachine.addRoutes(event: .continueWithPassword, transitions: [.serverConfirmationScreen => .loginScreen,
stateMachine.addRoutes(event: .continueWithPassword, transitions: [.serverSelectionScreen => .loginScreen,
.startScreen => .loginScreen]) { [weak self] context in
let loginHint = context.userInfo as? String
self?.showLoginScreen(loginHint: loginHint, fromState: context.fromState)
}
stateMachine.addRoutes(event: .cancelledPasswordLogin(previousState: .serverConfirmationScreen), transitions: [.loginScreen => .serverConfirmationScreen])
stateMachine.addRoutes(event: .cancelledPasswordLogin(previousState: .serverSelectionScreen), transitions: [.loginScreen => .serverSelectionScreen])
stateMachine.addRoutes(event: .cancelledPasswordLogin(previousState: .startScreen), transitions: [.loginScreen => .startScreen])
// Bug Report
@@ -243,7 +228,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
// Completion
stateMachine.addRoutes(event: .signedIn, transitions: [.qrCodeLoginScreen => .complete,
.serverConfirmationScreen => .complete, // OAuth authentication
.serverSelectionScreen => .complete, // OAuth authentication
.startScreen => .complete, // Direct OAuth authentication
.loginScreen => .complete]) { [weak self] context in
guard let userSession = context.userInfo as? UserSessionProtocol else { fatalError("The user session wasn't included in the context") }
@@ -292,9 +277,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
case .loginWithQR:
stateMachine.tryEvent(.loginWithQR)
case .login:
stateMachine.tryEvent(.confirmServer(.login))
stateMachine.tryEvent(.selectServer(.login))
case .register:
stateMachine.tryEvent(.confirmServer(.register))
stateMachine.tryEvent(.selectServer(.register))
case .loginDirectlyWithOAuth(let oAuthData, let window):
showOAuthAuthentication(oAuthData: oAuthData, presentationAnchor: window)
@@ -336,7 +321,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
case .signInManually:
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.cancelledLoginWithQR)
stateMachine.tryEvent(.confirmServer(.login))
stateMachine.tryEvent(.selectServer(.login))
case .signedIn(let userSession):
navigationStackCoordinator.setSheetCoordinator(nil)
DispatchQueue.main.async {
@@ -355,42 +340,15 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - Manual Authentication
private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) {
private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow) {
// Reset the service back to the default homeserver before continuing. This ensures
// we check that registration is supported if it was previously configured for login.
authenticationService.reset()
let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService,
authenticationFlow: authenticationFlow,
appSettings: appSettings,
userIndicatorController: userIndicatorController)
let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .continueWithOAuth(let oAuthData, let window):
showOAuthAuthentication(oAuthData: oAuthData, presentationAnchor: window)
case .continueWithPassword:
stateMachine.tryEvent(.continueWithPassword)
case .changeServer:
stateMachine.tryEvent(.changeServer(authenticationFlow))
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.cancelledServerConfirmation)
}
}
private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow) {
let navigationCoordinator = NavigationStackCoordinator()
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
authenticationFlow: authenticationFlow,
appSettings: appSettings,
homeserverHistoryManager: homeserverHistoryManager,
userIndicatorController: userIndicatorController)
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
@@ -399,17 +357,16 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return }
switch action {
case .updated:
navigationStackCoordinator.setSheetCoordinator(nil)
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .continueWithOAuth(let oAuthData, let window):
showOAuthAuthentication(oAuthData: oAuthData, presentationAnchor: window)
case .continueWithPassword:
stateMachine.tryEvent(.continueWithPassword)
}
}
.store(in: &cancellables)
navigationCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissedServerSelection)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.cancelledServerSelection)
}
}
@@ -507,5 +464,8 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private func userHasSignedIn(userSession: UserSessionProtocol) {
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
let newServer = authenticationService.homeserver.value.address
homeserverHistoryManager.addServerToList(newServer)
}
}