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
@@ -37,7 +37,7 @@ struct DefaultRemoteSettingsHook: RemoteSettingsHookProtocol {
let wellKnown = try JSONDecoder().decode(ElementWellKnown.self, from: wellKnownData)
if wellKnown.enforceElementPro == true {
let serverName = client.server() ?? client.homeserver()
let displayableServerName = LoginHomeserver(address: serverName, loginMode: .unknown).address
let displayableServerName = AccountProvider.generic(serverName).serverNameOrBaseURL
return .failure(.elementProRequired(serverName: displayableServerName))
} else {
return .success(())
@@ -0,0 +1,65 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
nonisolated enum AccountProvider: Equatable {
/// An account provider that was input/chosen by the user.
case generic(String)
/// An account provider as provided by the app.
case managed(serverName: String, baseURL: URL, canUseServerName: Bool = true)
/// The account provider's string value, shown in the UI and used to build a `Client` with.
var serverNameOrBaseURL: String {
let serverNameOrBaseURL = switch self {
case .generic(let serverNameOrBaseURL):
serverNameOrBaseURL
case .managed(let serverName, let baseURL, let canUseServerName):
canUseServerName ? serverName : baseURL.absoluteString
}
return sanitize(serverNameOrBaseURL)
}
var isGeneric: Bool {
switch self {
case .generic: true
case .managed: false
}
}
/// Sanitizes an address with the following rules:
/// - Trim any whitespace.
/// - Lowercase the address.
/// - Removes an https scheme (treating it as implicit).
/// - Remove any trailing slashes.
private func sanitize(_ serverNameOrBaseURL: String) -> String {
serverNameOrBaseURL
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.trimmingPrefix("https://") // Intentionally continue to show http:// as an indicator (and for history).
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
}
}
// MARK: - Array
extension [AccountProvider] {
/// Whether the collection includes a provider matching the given server name or base URL.
func contains(serverName: String?, orBaseURL baseURL: URL?) -> Bool {
contains { accountProvider in
switch accountProvider {
case .generic(let serverNameOrBaseURL):
// Lower case potential user input to be safe. Full sanitisation is wrong given the scheme would be ignored.
serverNameOrBaseURL.lowercased() == serverName || serverNameOrBaseURL.lowercased() == baseURL?.absoluteString
case .managed(let providerServerName, let providerBaseURL, _):
// We can ignore canUseServerName here, it's about constructing a client rather than matching.
providerServerName == serverName || providerBaseURL == baseURL
}
}
}
}
@@ -78,7 +78,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
// MARK: - Hooks
// swiftlint:disable:next function_parameter_count
func override(accountProviders: [String],
func override(accountProviders: [AccountProvider],
allowOtherAccountProviders: Bool,
hideBrandChrome: Bool,
pushGatewayBaseURL: URL,
@@ -143,7 +143,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
///
/// Account provider is the friendly term for the server name. It should not contain an `https` prefix and should
/// match the last part of the user ID. For example `example.com` and not `https://matrix.example.com`.
private(set) var accountProviders = ["matrix.org"]
private(set) var accountProviders: [AccountProvider] = [.managed(serverName: "matrix.org", baseURL: "https://matrix-client.matrix.org")]
/// Whether or not the user is allowed to manually enter their own account provider or must select from one of `defaultAccountProviders`.
private(set) var allowOtherAccountProviders = true
/// Whether the components surrounding the app brand/logo should be hidden or not
@@ -188,8 +188,12 @@ final nonisolated class AppSettings: @unchecked Sendable {
@UserPreference(key: "previousServers", defaultValue: [])
var previousServers: [String]
var defaultServer: String {
previousServers.first ?? accountProviders[0]
var defaultAccountProvider: AccountProvider {
if allowOtherAccountProviders {
previousServers.first.map { .generic($0) } ?? accountProviders[0]
} else {
accountProviders[0]
}
}
// MARK: - Security
@@ -465,7 +465,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private func userHasSignedIn(userSession: UserSessionProtocol) {
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
let newServer = authenticationService.homeserver.value.address
homeserverHistoryManager.addServerToList(newServer)
let newAccountProvider = authenticationService.homeserver.value.accountProvider
homeserverHistoryManager.addServerToList(newAccountProvider.serverNameOrBaseURL)
}
}
@@ -46,14 +46,14 @@ extension ClientFactoryMock {
convenience init(_ configuration: Configuration) {
self.init()
makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = { address, _, _, _, _, _ in
makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = { address, _, _, _, _, _ in
guard let client = configuration.homeserverClients[address] else {
throw ClientBuildError.ServerUnreachable(message: "Not a known homeserver.")
}
return client
}
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure = { address, _, _, _ in
makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksClosure = { address, _, _, _ in
guard let client = configuration.homeserverClients[address] else {
throw ClientBuildError.ServerUnreachable(message: "Not a known homeserver.")
}
@@ -2080,94 +2080,94 @@ nonisolated class ClientFactoryMock: ClientFactoryProtocol, @unchecked Sendable
//MARK: - makeAuthenticationClient
nonisolated(unsafe) var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksThrowableError: Error?
private let makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0
var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount: Int {
get { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount } }
set { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue } }
nonisolated(unsafe) var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksThrowableError: Error?
private let makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0
var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount: Int {
get { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount } }
set { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue } }
}
var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCalled: Bool {
return makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount > 0
var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCalled: Bool {
return makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount > 0
}
private let makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments: (homeserverAddress: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)?
var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments: (homeserverAddress: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)? {
get { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments } }
set { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments = newValue } }
private let makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments: (serverNameOrBaseURL: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)?
var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments: (serverNameOrBaseURL: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)? {
get { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments } }
set { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments = newValue } }
}
private let makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations: [(homeserverAddress: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = []
var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(homeserverAddress: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] {
get { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations } }
set { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations = newValue } }
private let makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations: [(serverNameOrBaseURL: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = []
var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(serverNameOrBaseURL: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] {
get { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations } }
set { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations = newValue } }
}
private let makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValueLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: ClientProtocol!
var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue: ClientProtocol! {
get { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue } }
set { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue } }
private let makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValueLock = NSLock()
private nonisolated(unsafe) var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: ClientProtocol!
var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue: ClientProtocol! {
get { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue } }
set { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue } }
}
nonisolated(unsafe) var makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure: ((String, SessionDirectories, String, ClientSessionDelegate, AppSettings, AppHooks) async throws -> ClientProtocol)?
nonisolated(unsafe) var makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure: ((String, SessionDirectories, String, ClientSessionDelegate, AppSettings, AppHooks) async throws -> ClientProtocol)?
@concurrent func makeAuthenticationClient(homeserverAddress: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) async throws -> ClientProtocol {
if let error = makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksThrowableError {
@concurrent func makeAuthenticationClient(serverNameOrBaseURL: String, sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) async throws -> ClientProtocol {
if let error = makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksThrowableError {
throw error
}
makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount += 1 }
makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments = (homeserverAddress: homeserverAddress, sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)
makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations.append((homeserverAddress: homeserverAddress, sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)) }
if let makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure {
return try await makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure(homeserverAddress, sessionDirectories, passphrase, clientSessionDelegate, appSettings, appHooks)
makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount += 1 }
makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments = (serverNameOrBaseURL: serverNameOrBaseURL, sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)
makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations.append((serverNameOrBaseURL: serverNameOrBaseURL, sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)) }
if let makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure {
return try await makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure(serverNameOrBaseURL, sessionDirectories, passphrase, clientSessionDelegate, appSettings, appHooks)
} else {
return makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue
return makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue
}
}
//MARK: - makeInMemoryClient
nonisolated(unsafe) var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksThrowableError: Error?
private let makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCountLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCount: Int {
get { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount } }
set { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue } }
nonisolated(unsafe) var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksThrowableError: Error?
private let makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksCallsCountLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0
var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksCallsCount: Int {
get { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount } }
set { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue } }
}
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCalled: Bool {
return makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCount > 0
var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksCalled: Bool {
return makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksCallsCount > 0
}
private let makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments: (homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)?
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedArguments: (homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)? {
get { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments } }
set { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments = newValue } }
private let makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments: (serverNameOrBaseURL: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)?
var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedArguments: (serverNameOrBaseURL: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)? {
get { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments } }
set { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedArgumentsLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedArguments = newValue } }
}
private let makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations: [(homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = []
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] {
get { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations } }
set { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations = newValue } }
private let makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations: [(serverNameOrBaseURL: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = []
var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(serverNameOrBaseURL: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] {
get { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations } }
set { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations = newValue } }
}
private let makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReturnValueLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: ClientProtocol!
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReturnValue: ClientProtocol! {
get { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue } }
set { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue } }
private let makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReturnValueLock = NSLock()
private nonisolated(unsafe) var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: ClientProtocol!
var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReturnValue: ClientProtocol! {
get { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue } }
set { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReturnValueLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue } }
}
nonisolated(unsafe) var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure: ((String, ClientSessionDelegate, AppSettings, AppHooks) async throws -> ClientProtocol)?
nonisolated(unsafe) var makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksClosure: ((String, ClientSessionDelegate, AppSettings, AppHooks) async throws -> ClientProtocol)?
@concurrent func makeInMemoryClient(homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) async throws -> ClientProtocol {
if let error = makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksThrowableError {
@concurrent func makeInMemoryClient(serverNameOrBaseURL: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) async throws -> ClientProtocol {
if let error = makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksThrowableError {
throw error
}
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount += 1 }
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedArguments = (homeserverAddress: homeserverAddress, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations.append((homeserverAddress: homeserverAddress, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)) }
if let makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure = makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure {
return try await makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure(homeserverAddress, clientSessionDelegate, appSettings, appHooks)
makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksCallsCountLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount += 1 }
makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedArguments = (serverNameOrBaseURL: serverNameOrBaseURL, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)
makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReceivedInvocationsLock.withLock { makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksUnderlyingReceivedInvocations.append((serverNameOrBaseURL: serverNameOrBaseURL, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)) }
if let makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksClosure = makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksClosure {
return try await makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksClosure(serverNameOrBaseURL, clientSessionDelegate, appSettings, appHooks)
} else {
return makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReturnValue
return makeInMemoryClientServerNameOrBaseURLClientSessionDelegateAppSettingsAppHooksReturnValue
}
}
//MARK: - makeAppClient
@@ -10,35 +10,10 @@ import Foundation
/// Information about a homeserver that is ready for display in the authentication flow.
struct LoginHomeserver: Equatable {
/// The homeserver string to be shown to the user.
let address: String
/// The account provider who operates the homeserver.
let accountProvider: AccountProvider
/// The types login supported by the homeserver.
var loginMode: LoginMode
/// Creates a new homeserver value.
init(address: String, loginMode: LoginMode) {
let address = Self.sanitized(address).components(separatedBy: "://").last ?? address
self.address = address
self.loginMode = loginMode
}
/// Sanitizes a user entered homeserver address with the following rules
/// - Trim any whitespace.
/// - Lowercase the address.
/// - Ensure the address contains a scheme, otherwise make it `https`.
/// - Remove any trailing slashes.
static func sanitized(_ address: String) -> String {
var address = address.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if !address.contains("://") {
address = "https://\(address)"
}
address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
return address
}
}
// MARK: - Mocks
@@ -46,21 +21,21 @@ struct LoginHomeserver: Equatable {
extension LoginHomeserver {
/// A mock homeserver that is configured just like matrix.org.
static var mockMatrixDotOrg: LoginHomeserver {
LoginHomeserver(address: "matrix.org", loginMode: .oAuth(supportsCreatePrompt: true))
LoginHomeserver(accountProvider: .generic("matrix.org"), loginMode: .oAuth(supportsCreatePrompt: true))
}
/// A mock homeserver that supports login and registration via a password but has no OAuth support.
static var mockBasicServer: LoginHomeserver {
LoginHomeserver(address: "example.com", loginMode: .password)
LoginHomeserver(accountProvider: .generic("example.com"), loginMode: .password)
}
/// A mock homeserver that supports only supports authentication via OAuth.
static var mockOAuth: LoginHomeserver {
LoginHomeserver(address: "company.com", loginMode: .oAuth(supportsCreatePrompt: false))
LoginHomeserver(accountProvider: .generic("company.com"), loginMode: .oAuth(supportsCreatePrompt: false))
}
/// A mock homeserver that only with no supported login flows.
static var mockUnsupported: LoginHomeserver {
LoginHomeserver(address: "server.net", loginMode: .unsupported)
LoginHomeserver(accountProvider: .generic("server.net"), loginMode: .unsupported)
}
}
@@ -50,7 +50,7 @@ struct LoginScreen: View {
BigIcon(icon: \.lockSolid)
.padding(.bottom, 8)
Text(L10n.screenLoginTitleWithHomeserver(context.viewState.homeserver.address))
Text(L10n.screenLoginTitleWithHomeserver(context.viewState.homeserver.accountProvider.serverNameOrBaseURL))
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)
@@ -127,7 +127,7 @@ struct LoginScreen: View {
struct LoginScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = makeViewModel()
static let credentialsViewModel = makeViewModel(withCredentials: true)
static let unconfiguredViewModel = makeViewModel(homeserverAddress: "somethingtofailconfiguration")
static let unconfiguredViewModel = makeViewModel(serverNameOrBaseURL: "somethingtofailconfiguration")
static var previews: some View {
ElementNavigationStack {
@@ -148,10 +148,10 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
.previewDisplayName("Unsupported")
}
static func makeViewModel(homeserverAddress: String = "example.com", withCredentials: Bool = false) -> LoginScreenViewModel {
static func makeViewModel(serverNameOrBaseURL: String = "example.com", withCredentials: Bool = false) -> LoginScreenViewModel {
let authenticationService = AuthenticationService.mock
Task { await authenticationService.configure(for: homeserverAddress, flow: .login) }
Task { await authenticationService.configure(for: serverNameOrBaseURL, flow: .login) }
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
loginHint: nil,
@@ -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
@@ -24,7 +24,7 @@ struct AuthenticationStartScreenViewState: BindableState {
/// The presentation anchor used for OAuth authentication.
var window: UIWindow?
let serverName: String?
let serverNameOrBaseURL: String?
let showCreateAccountButton: Bool
let showQRCodeLoginButton: Bool
@@ -36,8 +36,8 @@ struct AuthenticationStartScreenViewState: BindableState {
var bindings = AuthenticationStartScreenViewStateBindings()
var loginButtonTitle: String {
if let serverName {
L10n.screenOnboardingSignInTo(serverName)
if let serverNameOrBaseURL {
L10n.screenOnboardingSignInTo(serverNameOrBaseURL)
} else if showQRCodeLoginButton {
L10n.screenOnboardingSignInManually
} else {
@@ -40,27 +40,30 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
canReportProblem = isBugReportServiceEnabled
let isQRCodeScanningSupported = !ProcessInfo.processInfo.isiOSAppOnMac
let classicAppAccountProvider = authenticationService.classicAppAccount?.serverName
let isClassicAppAccountAllowed = classicAppAccountProvider.map { appSettings.accountProviders.contains($0) } ?? false
let isClassicAppAccountAllowed = if let classicAppAccount = authenticationService.classicAppAccount {
appSettings.accountProviders.contains(serverName: classicAppAccount.serverName, orBaseURL: classicAppAccount.homeserverURL)
} else {
false
}
let initialViewState = if !appSettings.allowOtherAccountProviders {
// We don't show the create account button when custom providers are disallowed.
// The assumption here being that if you're running a custom app, your users will already be created.
AuthenticationStartScreenViewState(serverName: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0] : nil,
AuthenticationStartScreenViewState(serverNameOrBaseURL: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0].serverNameOrBaseURL : nil,
showCreateAccountButton: false,
showQRCodeLoginButton: isQRCodeScanningSupported,
classicAppMode: isClassicAppAccountAllowed ? authenticationService.classicAppAccount.map { .welcomeBack($0) } : nil,
hideBrandChrome: appSettings.hideBrandChrome)
} else if let provisioningParameters {
// We only show the "Sign in to …" button when using a provisioning link.
AuthenticationStartScreenViewState(serverName: provisioningParameters.accountProvider,
AuthenticationStartScreenViewState(serverNameOrBaseURL: provisioningParameters.accountProvider,
showCreateAccountButton: false,
showQRCodeLoginButton: false,
classicAppMode: nil,
hideBrandChrome: appSettings.hideBrandChrome)
} else {
// The default configuration.
AuthenticationStartScreenViewState(serverName: nil,
AuthenticationStartScreenViewState(serverNameOrBaseURL: nil,
showCreateAccountButton: appSettings.showCreateAccountButton,
showQRCodeLoginButton: isQRCodeScanningSupported,
classicAppMode: authenticationService.classicAppAccount.map { .welcomeBack($0) },
@@ -114,22 +117,22 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
if classicAppAccount.state.availableSecrets == .requiresBackup {
state.bindings.showClassicAppBackupInstructions = true
} else {
await configureAccountProvider(classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL)
await loginDirectly(using: classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL)
}
} else if let serverName = state.serverName {
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
} else if let serverNameOrBaseURL = state.serverNameOrBaseURL {
await loginDirectly(using: serverNameOrBaseURL, loginHint: provisioningParameters?.loginHint)
} else {
actionsSubject.send(.login) // No need to configure anything here, continue the flow.
}
}
private func configureAccountProvider(_ accountProvider: String, loginHint: String? = nil, fallbackHomeserverURL: URL? = nil) async {
private func loginDirectly(using serverNameOrBaseURL: String, loginHint: String? = nil, fallbackHomeserverURL: URL? = nil) async {
startLoading()
defer { stopLoading() }
if case .failure = await authenticationService.configure(for: accountProvider, flow: .login) {
if case .failure = await authenticationService.configure(for: serverNameOrBaseURL, flow: .login) {
// Try the fallback URL before showing an error.
if let fallbackHomeserverURL,
case .success = await authenticationService.configure(for: fallbackHomeserverURL.absoluteString, flow: .login) {
@@ -22,6 +22,9 @@ class AuthenticationService: AuthenticationServiceProtocol {
private let appHooks: AppHooks
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
/// The currently configured homeserver.
///
/// **Note:** The value's `accountProvider` will always be `.generic` after calling `configure(for:flow:)`.
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> {
homeserverSubject.asCurrentValuePublisher()
}
@@ -64,17 +67,17 @@ class AuthenticationService: AuthenticationServiceProtocol {
}
// When updating these, don't forget to update the reset method too.
homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultServer, loginMode: .unknown))
homeserverSubject = .init(LoginHomeserver(accountProvider: appSettings.defaultAccountProvider, loginMode: .unknown))
flow = .login
}
// MARK: - Public
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError> {
func configure(for serverNameOrBaseURL: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError> {
do {
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
var homeserver = LoginHomeserver(accountProvider: .generic(serverNameOrBaseURL), loginMode: .unknown)
let client = try await makeClient(homeserverAddress: homeserverAddress)
let client = try await makeClient(serverNameOrBaseURL: serverNameOrBaseURL)
let loginDetails = await client.homeserverLoginDetails()
homeserver.loginMode = if loginDetails.supportsOauthLogin() {
@@ -106,7 +109,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
return .failure(.elementProRequired(serverName: serverName))
} catch {
MXLog.error("Failed configuring a server: \(error)")
return .failure(.invalidHomeserverAddress)
return .failure(.invalidServerNameOrBaseURL)
}
}
@@ -192,8 +195,8 @@ class AuthenticationService: AuthenticationServiceProtocol {
// n.b. We rely on the SDK checking that the intent of the QR is suitable for us to login with.
// Future versions of the QR should always give us the baseUrl
guard let scannedServerNameOrBaseUrl = qrData.baseUrl() ?? qrData.serverName() else {
// Future versions of the QR should always give us the baseURL
guard let scannedServerNameOrBaseURL = qrData.baseUrl() ?? qrData.serverName() else {
// With the older version of QR we treat the presence of serverName as meaning that the other device
// is not signed in.
MXLog.error("The QR code is from a device that is not yet signed in.")
@@ -201,7 +204,20 @@ class AuthenticationService: AuthenticationServiceProtocol {
return progressSubject.asCurrentValuePublisher()
}
// n.b. We deliberatley don't check whether the received server is in our appSettings.accountProviders
// When account provider selection is restricted, make sure the scanned server is allowed.
// However, generic providers aren't detailed enough, so continue if one is present.
if !appSettings.allowOtherAccountProviders, !appSettings.accountProviders.contains(where: \.isGeneric) {
// A base URL from the QR code is expected to be a valid URL when present.
let scannedBaseURL = qrData.baseUrl().flatMap(URL.init(string:))
guard appSettings.accountProviders.contains(serverName: qrData.serverName(), orBaseURL: scannedBaseURL) else {
MXLog.error("The scanned QR code is for an account provider that isn't allowed.")
let error = QRCodeLoginError.providerNotAllowed(scannedProvider: scannedServerNameOrBaseURL,
allowedProviders: appSettings.accountProviders.map(\.serverNameOrBaseURL))
progressSubject.send(completion: .failure(.qrCodeError(error)))
return progressSubject.asCurrentValuePublisher()
}
}
// The SDK calls the listener from arbitrary threads; onMainActor forwards the progress
// updates on the main actor in FIFO order.
@@ -212,7 +228,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
Task {
do {
let client = try await makeClient(homeserverAddress: scannedServerNameOrBaseUrl)
let client = try await makeClient(serverNameOrBaseURL: scannedServerNameOrBaseURL)
let qrCodeHandler = client.newLoginWithQrCodeHandler(oauthConfiguration: appSettings.oAuthConfiguration.rustValue)
try await qrCodeHandler.scan(qrCodeData: qrData, progressListener: listener)
@@ -240,19 +256,19 @@ class AuthenticationService: AuthenticationServiceProtocol {
}
func reset() {
homeserverSubject.send(LoginHomeserver(address: appSettings.defaultServer, loginMode: .unknown))
homeserverSubject.send(LoginHomeserver(accountProvider: appSettings.defaultAccountProvider, loginMode: .unknown))
flow = .login
client = nil
}
// MARK: - Private
private func makeClient(homeserverAddress: String) async throws -> ClientProtocol {
private func makeClient(serverNameOrBaseURL: String) async throws -> ClientProtocol {
// Use a fresh session directory each time the user enters a different server
// so that caches (e.g. server versions) are always fresh for the new server.
rotateSessionDirectory()
let client = try await clientFactory.makeAuthenticationClient(homeserverAddress: homeserverAddress,
let client = try await clientFactory.makeAuthenticationClient(serverNameOrBaseURL: serverNameOrBaseURL,
sessionDirectories: sessionDirectories,
passphrase: passphrase,
clientSessionDelegate: userSessionStore.clientSessionDelegate,
@@ -286,7 +302,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
MXLog.info("Checking Classic app account: \(classicAppAccount)")
do {
let client = try await clientFactory.makeInMemoryClient(homeserverAddress: classicAppAccount.homeserverURL.absoluteString,
let client = try await clientFactory.makeInMemoryClient(serverNameOrBaseURL: classicAppAccount.homeserverURL.absoluteString,
clientSessionDelegate: userSessionStore.clientSessionDelegate,
appSettings: appSettings,
appHooks: appHooks)
@@ -26,7 +26,7 @@ enum AuthenticationServiceError: Error, Equatable {
case invalidServer
case invalidCredentials
case invalidHomeserverAddress
case invalidServerNameOrBaseURL
case invalidWellKnown(String)
case slidingSyncNotAvailable
case loginNotSupported
@@ -39,12 +39,14 @@ enum AuthenticationServiceError: Error, Equatable {
protocol AuthenticationServiceProtocol: QRCodeLoginServiceProtocol {
/// The currently configured homeserver.
///
/// **Note:** The value's `accountProvider` will always be `.generic` after calling `configure(for:flow:)`.
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
/// The type of flow the service is currently configured with.
var flow: AuthenticationFlow { get }
/// Sets up the service for login on the specified homeserver address.
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
/// Sets up the service for login or registration using the supplied server name or base URL.
func configure(for serverNameOrBaseURL: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
/// Performs login using OAuth for the current homeserver.
func urlForOAuthLogin(loginHint: String?) async -> Result<OAuthAuthorizationDataProxy, AuthenticationServiceError>
/// Asks the SDK to abort an ongoing OAuth login if we didn't get a callback to complete the request with.
@@ -14,7 +14,7 @@ nonisolated struct ClientFactory: ClientFactoryProtocol {
// MARK: Authentication
#if IS_MAIN_APP
func makeAuthenticationClient(homeserverAddress: String,
func makeAuthenticationClient(serverNameOrBaseURL: String,
sessionDirectories: SessionDirectories,
passphrase: String,
clientSessionDelegate: ClientSessionDelegate,
@@ -29,12 +29,12 @@ nonisolated struct ClientFactory: ClientFactoryProtocol {
.enableAutomaticBackPagination(enableAutomaticBackPagination: appSettings.automaticBackPaginationEnabled)
.sqliteStore(config: .init(dataPath: sessionDirectories.dataPath, cachePath: sessionDirectories.cachePath)
.passphrase(passphrase: passphrase))
.serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress)
.serverNameOrHomeserverUrl(serverNameOrUrl: serverNameOrBaseURL)
return try await build(builder, for: .authentication, appHooks: appHooks)
}
func makeInMemoryClient(homeserverAddress: String,
func makeInMemoryClient(serverNameOrBaseURL: String,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) async throws -> ClientProtocol {
@@ -45,7 +45,7 @@ nonisolated struct ClientFactory: ClientFactoryProtocol {
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
threadsEnabled: appSettings.threadsEnabled)
.inMemoryStore()
.serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress)
.serverNameOrHomeserverUrl(serverNameOrUrl: serverNameOrBaseURL)
return try await build(builder, for: .classicAppAccount, appHooks: appHooks)
}
@@ -12,14 +12,14 @@ nonisolated protocol ClientFactoryProtocol {
// MARK: Authentication
#if IS_MAIN_APP
func makeAuthenticationClient(homeserverAddress: String,
func makeAuthenticationClient(serverNameOrBaseURL: String,
sessionDirectories: SessionDirectories,
passphrase: String,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) async throws -> ClientProtocol
func makeInMemoryClient(homeserverAddress: String,
func makeInMemoryClient(serverNameOrBaseURL: String,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) async throws -> ClientProtocol
@@ -67,7 +67,7 @@ class HomeserverHistoryManager {
/// in all lowercase. This is the list that matches are made against.
private func updateCachedServers() {
let previous = appSettings.previousServers.map { $0.lowercased() }
let defaultProviders = appSettings.accountProviders.map { $0.lowercased() }
let defaultProviders = appSettings.accountProviders.map { $0.serverNameOrBaseURL.lowercased() }
cachedServers = previous + defaultProviders
}
@@ -150,7 +150,12 @@ class MockScreen: Identifiable {
let appSettings: AppSettings! = appSettings
if id == .singleProviderAuthenticationFlow || id == .multipleProvidersAuthenticationFlow {
let accountProviders = id == .singleProviderAuthenticationFlow ? ["example.com"] : ["guest.example.com", "example.com"]
let accountProviders: [AccountProvider] = if id == .singleProviderAuthenticationFlow {
[.managed(serverName: "example.com", baseURL: "https://matrix.example.com")]
} else {
[.managed(serverName: "guest.example.com", baseURL: "https://matrix.guest.example.com"),
.managed(serverName: "example.com", baseURL: "https://matrix.example.com")]
}
appSettings.override(accountProviders: accountProviders,
allowOtherAccountProviders: false,
hideBrandChrome: false,