Add an AccountProvider type and use it within the authentication flow. (#6103)

* Add an AccountProvider type and use it within AppSettings.

* Use the AccountProvider type within the Authentication flow too.

* Fix unit test compilation with Swift 6.4

* Make sure defaultAccountProvider takes allowOtherAccountProviders into account.

* Document a caveat on using AuthenticationService.homeserver.value.accountProvider.
This commit is contained in:
Doug
2026-09-14 15:25:33 +01:00
committed by GitHub
parent 26fddb8aa2
commit 64aae09c6d
29 changed files with 442 additions and 265 deletions
@@ -41,7 +41,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
let mode: ServerSelectionScreenMode = if parameters.appSettings.allowOtherAccountProviders {
.userInput
} else {
.picker(parameters.appSettings.accountProviders)
.picker(parameters.appSettings.accountProviders.map(\.serverNameOrBaseURL))
}
viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService,
@@ -88,18 +88,18 @@ struct ServerSelectionScreenViewState: BindableState {
var hasValidationError: Bool {
switch mode {
case .userInput:
bindings.homeserverAddress.isEmpty || isShowingFooterError
bindings.serverNameOrBaseURL.isEmpty || isShowingFooterError
case .picker(let options):
!options.contains(bindings.homeserverAddress) || isShowingFooterError
!options.contains(bindings.serverNameOrBaseURL) || isShowingFooterError
}
}
}
struct ServerSelectionScreenBindings {
/// The homeserver address input or chosen by the user.
var homeserverAddress: String
/// The selection range in `homeserverAddress`
var homeserverSelection: TextSelection?
/// The homeserver input or chosen by the user.
var serverNameOrBaseURL: String
/// The selection range in `serverNameOrBaseURL`
var serverNameOrBaseURLSelection: TextSelection?
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<ServerSelectionScreenErrorType>?
@@ -41,13 +41,12 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
self.userIndicatorController = userIndicatorController
self.homeserverHistoryManager = homeserverHistoryManager
let homeserverAddress: String
if case .picker(let providers) = mode {
homeserverAddress = providers[0]
let serverNameOrBaseURL = if case .picker(let providers) = mode {
providers[0]
} else {
homeserverAddress = authenticationService.homeserver.value.address
authenticationService.homeserver.value.accountProvider.serverNameOrBaseURL
}
let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress)
let bindings = ServerSelectionScreenBindings(serverNameOrBaseURL: serverNameOrBaseURL)
super.init(initialViewState: ServerSelectionScreenViewState(mode: mode, authenticationFlow: authenticationFlow, bindings: bindings))
context.viewState.textFieldAdapter.keystrokePublisher
@@ -80,40 +79,40 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
// MARK: - Private
private func pickServer() async {
let accountProvider = state.bindings.homeserverAddress
guard accountProvider.isEmpty == false else {
let serverNameOrBaseURL = state.bindings.serverNameOrBaseURL
guard serverNameOrBaseURL.isEmpty == false else {
fatalError("It shouldn't be possible to confirm without a selection.")
}
startLoading()
switch await authenticationService.configure(for: accountProvider, flow: authenticationFlow) {
switch await authenticationService.configure(for: serverNameOrBaseURL, flow: authenticationFlow) {
case .success:
MXLog.info("Selected server: \(accountProvider)")
MXLog.info("Selected server: \(serverNameOrBaseURL)")
await fetchLoginURLIfNeededAndContinue()
stopLoading()
case .failure:
MXLog.info("Invalid server: \(accountProvider)")
MXLog.info("Invalid server: \(serverNameOrBaseURL)")
stopLoading()
// When the servers are hard-coded they should have a valid configuration, so show a generic error.
state.bindings.alertInfo = AlertInfo(id: .unknownError)
}
}
/// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
/// Updates the login flow using the supplied server name or base URL, or shows an error when this isn't possible.
private func configureHomeserver() async {
let userInput = state.bindings.homeserverAddress
let userInput = state.bindings.serverNameOrBaseURL
// People often enter their Matrix ID here, so use the server name from it when they do.
let homeserverAddress = (try? serverNameFromUserId(userId: userInput)) ?? userInput
let serverNameOrBaseURL = (try? serverNameFromUserId(userId: userInput)) ?? userInput
startLoading()
switch await authenticationService.configure(for: homeserverAddress, flow: authenticationFlow) {
switch await authenticationService.configure(for: serverNameOrBaseURL, flow: authenticationFlow) {
case .success:
MXLog.info("Selected homeserver: \(homeserverAddress)")
MXLog.info("Selected homeserver: \(serverNameOrBaseURL)")
await fetchLoginURLIfNeededAndContinue()
stopLoading()
case .failure(let error):
MXLog.info("Invalid homeserver: \(homeserverAddress)")
MXLog.info("Invalid homeserver: \(serverNameOrBaseURL)")
stopLoading()
handleError(error)
}
@@ -153,7 +152,7 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidServer, .invalidHomeserverAddress:
case .invalidServer, .invalidServerNameOrBaseURL:
showFooterMessage(L10n.screenChangeServerErrorInvalidHomeserver)
case .invalidWellKnown(let error):
state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error),
@@ -221,9 +220,9 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
debouncedAutocompleteTask = Task { @MainActor in
try? await Task.sleep(for: .milliseconds(10))
guard Task.isCancelled == false else { return }
context.homeserverAddress = newInput
context.serverNameOrBaseURL = newInput
context.viewState.textField?.text = newInput
context.homeserverSelection = TextSelection(range: selectionRange)
context.serverNameOrBaseURLSelection = TextSelection(range: selectionRange)
}
}
}
@@ -67,7 +67,7 @@ struct ServerSelectionScreen: View {
VStack(alignment: .leading, spacing: 24) {
switch context.viewState.mode {
case .userInput:
TextField(L10n.screenChangeServerTextfieldPlaceholder, text: $context.homeserverAddress, selection: $context.homeserverSelection)
TextField(L10n.screenChangeServerTextfieldPlaceholder, text: $context.serverNameOrBaseURL, selection: $context.serverNameOrBaseURLSelection)
.textFieldStyle(.compound(labelText: Text(L10n.screenChangeServerTextfieldHeader),
footerText: Text(context.viewState.footerMessage),
state: context.viewState.isShowingFooterError ? .error : .default,
@@ -78,13 +78,13 @@ struct ServerSelectionScreen: View {
.introspect(.textField, on: .supportedVersions) {
context.send(viewAction: .updateTextField($0))
}
.onChange(of: context.homeserverAddress) { context.send(viewAction: .clearFooterError) }
.onChange(of: context.serverNameOrBaseURL) { context.send(viewAction: .clearFooterError) }
.submitLabel(.done)
.onSubmit(submit)
case .picker(let providers):
FakeInlinePicker(items: providers,
icon: \.host,
selection: $context.homeserverAddress)
selection: $context.serverNameOrBaseURL)
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.serverPicker)
}
}
@@ -140,10 +140,10 @@ private struct FakeInlinePicker: View {
@available(iOS 26.0, *)
struct ServerSelection_Previews: PreviewProvider, TestablePreview {
static let matrixViewModel = makeViewModel(mode: .userInput, homeserverAddress: "matrix.org")
static let emptyViewModel = makeViewModel(mode: .userInput, homeserverAddress: "")
static let invalidViewModel = makeViewModel(mode: .userInput, homeserverAddress: "thisisbad")
static let pickerViewModel = makeViewModel(mode: .picker(["matrix.org", "foo.bar", "baz.me"]), homeserverAddress: "foo.bar")
static let matrixViewModel = makeViewModel(mode: .userInput, serverNameOrBaseURL: "matrix.org")
static let emptyViewModel = makeViewModel(mode: .userInput, serverNameOrBaseURL: "")
static let invalidViewModel = makeViewModel(mode: .userInput, serverNameOrBaseURL: "thisisbad")
static let pickerViewModel = makeViewModel(mode: .picker(["matrix.org", "foo.bar", "baz.me"]), serverNameOrBaseURL: "foo.bar")
static var previews: some View {
ElementNavigationStack {
@@ -168,7 +168,7 @@ struct ServerSelection_Previews: PreviewProvider, TestablePreview {
.previewDisplayName("Picker")
}
static func makeViewModel(mode: ServerSelectionScreenMode = .userInput, homeserverAddress: String) -> ServerSelectionScreenViewModel {
static func makeViewModel(mode: ServerSelectionScreenMode = .userInput, serverNameOrBaseURL: String) -> ServerSelectionScreenViewModel {
let authenticationService = AuthenticationService.mock
let appSettings = AppSettings.volatile()
@@ -179,8 +179,8 @@ struct ServerSelection_Previews: PreviewProvider, TestablePreview {
appSettings: appSettings,
homeserverHistoryManager: HomeserverHistoryManager(appSettings: appSettings),
userIndicatorController: UserIndicatorControllerMock())
viewModel.context.homeserverAddress = homeserverAddress
if case .userInput = mode, homeserverAddress == "thisisbad" {
viewModel.context.serverNameOrBaseURL = serverNameOrBaseURL
if case .userInput = mode, serverNameOrBaseURL == "thisisbad" {
viewModel.context.send(viewAction: .confirm)
}
return viewModel