diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 180578143..98b52e22e 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 039FE1757084092146C0B616 /* ClientFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176A581FDC61737676782607 /* ClientFactoryProtocol.swift */; }; 03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */; }; + 03E9750B2F994A79FA1296A1 /* AccountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67BCEFE35F8D00CADF108C9 /* AccountProvider.swift */; }; 03F192C5C011AFA6068C9EAA /* SpaceServiceSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B1E4D1F0A6FAAE64B54D76 /* SpaceServiceSDKMock.swift */; }; 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */; }; 044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */; }; @@ -370,6 +371,7 @@ 3A0D1A201E2CD209A04CE89B /* SearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD63A653BCA2C8112654B19 /* SearchScreenModels.swift */; }; 3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */; }; 3A2640C52505BAB3D983A92A /* RemotePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A05E85E4872C3221C5C287 /* RemotePreference.swift */; }; + 3A363617757E6838AC3D15FB /* AccountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67BCEFE35F8D00CADF108C9 /* AccountProvider.swift */; }; 3A64A93A651A3CB8774ADE8E /* SwiftOGG in Frameworks */ = {isa = PBXBuildFile; productRef = 3FE40E79C36E7903121E6E3B /* SwiftOGG */; }; 3A68752BB9D51B8713C0FC2C /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A768CA51A59B8A5D8C8FD599 /* AuthenticationStartScreen.swift */; }; 3AA9E878FDCFF85664AC071F /* ComposerDraftService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D70253004A5AEC9C73D6A4F /* ComposerDraftService.swift */; }; @@ -736,6 +738,7 @@ 756EA0D663261889EF64E6D4 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */; }; 7573D682F089205F7F1D96CF /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; }; 757862045774A0F458357E19 /* RoomThreadListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544B9262E1BC6F489C03FFFA /* RoomThreadListScreen.swift */; }; + 75C1AFACE6C0DEA18F16456C /* AccountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67BCEFE35F8D00CADF108C9 /* AccountProvider.swift */; }; 75E55A763FE8BA64FB8513E4 /* TimelineStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F968FDB68F9D4CB9010FED70 /* TimelineStateTests.swift */; }; 75ED4B73983228BB6922CE3C /* KnockRequestsListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5C217DD0749EC709EED028 /* KnockRequestsListScreenViewModelProtocol.swift */; }; 761EA50B2619307AB30891B8 /* PhishingDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB07F03461023BC39C730922 /* PhishingDetector.swift */; }; @@ -1027,6 +1030,7 @@ A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; }; A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; }; A180F7B8048C3697453760F2 /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A98F895BD60A4448E2A278C /* SessionDirectories.swift */; }; + A1A16FEF565ED7972591988B /* AccountProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D0FBA1FE29833A915B77AE /* AccountProviderTests.swift */; }; A1BA8D6BABAFA9BAAEAA3FFD /* NotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */; }; A1DF0E1E526A981ED6D5DF44 /* UserIndicatorControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */; }; A2091F4B1332D9BF273B09D5 /* SpaceServiceProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */; }; @@ -1350,6 +1354,7 @@ D69CCEC785E6C819551B402F /* SearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5413213B56FB76C1071E2BFB /* SearchScreenViewModelTests.swift */; }; D6DE764B17FB4A9A12C33BF4 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */; }; D75C591C44C2510FDFA9C3D8 /* Macros in Frameworks */ = {isa = PBXBuildFile; productRef = AF574A0FC6F3A83D3BFFC9B3 /* Macros */; }; + D7A26828D8715A17A1D52E66 /* AppSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11D53EE8E666C25F97B4369 /* AppSettingsTests.swift */; }; D7CC123DC5138C3BF1F9999D /* MapLibreAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50642DAE43672897C0BBAE2 /* MapLibreAnnotation.swift */; }; D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; D820B3C223E4C2E77BB2A2BF /* ElementCallServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EA2AFF6EB59FE25234D29F3 /* ElementCallServiceTests.swift */; }; @@ -1856,6 +1861,7 @@ 127D1947BA9C6CA62E3D03EC /* DeclineAndBlockScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreen.swift; sourceTree = ""; }; 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 12B09A94C519227264A41208 /* RoomMembershipDetailsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembershipDetailsProxy.swift; sourceTree = ""; }; + 12D0FBA1FE29833A915B77AE /* AccountProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProviderTests.swift; sourceTree = ""; }; 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = ""; }; @@ -2691,6 +2697,7 @@ A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomCell.swift; sourceTree = ""; }; A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenKnockedCell.swift; sourceTree = ""; }; A1087DCC491CD4C027173DDA /* EmojiPickerScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModelTests.swift; sourceTree = ""; }; + A11D53EE8E666C25F97B4369 /* AppSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsTests.swift; sourceTree = ""; }; A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = ""; }; A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = ""; }; A14FD296A75F5F5637EDC365 /* RoomThreadListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreenCoordinator.swift; sourceTree = ""; }; @@ -2716,6 +2723,7 @@ A566F8F2C27B99E1FB80C69B /* JoinRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRule.swift; sourceTree = ""; }; A5C45578F406698388B97C07 /* AuthenticationClassicAppAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClassicAppAccountView.swift; sourceTree = ""; }; A5DA892E8643240C7BC41900 /* RoomListActivityVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListActivityVisibility.swift; sourceTree = ""; }; + A67BCEFE35F8D00CADF108C9 /* AccountProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProvider.swift; sourceTree = ""; }; A6B19D10B102956066AF117B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; @@ -5178,9 +5186,11 @@ 73CD9796729EB702B4DFA88C /* Sources */ = { isa = PBXGroup; children = ( + 12D0FBA1FE29833A915B77AE /* AccountProviderTests.swift */, 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */, C687844F60BFF532D49A994C /* AnalyticsTests.swift */, E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */, + A11D53EE8E666C25F97B4369 /* AppSettingsTests.swift */, 893777A4997BBDB68079D4F5 /* ArrayTests.swift */, AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */, 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */, @@ -6833,6 +6843,7 @@ D64E20C9E4C486D5C764D7F3 /* Settings */ = { isa = PBXGroup; children = ( + A67BCEFE35F8D00CADF108C9 /* AccountProvider.swift */, FA2397174D0DC3918A7A8A7B /* AnalyticsConfiguration.swift */, 353024006CB726E9F9187B3A /* AnalyticsConsentState.swift */, 0FD22AEFFA20065494ED2333 /* AppAppearance.swift */, @@ -8165,6 +8176,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 75C1AFACE6C0DEA18F16456C /* AccountProvider.swift in Sources */, CA2BFC5504AD91A028CFA45B /* AnalyticsConfiguration.swift in Sources */, 4E9ED3E6E979C6C7FEC1D011 /* AnalyticsConsentState.swift in Sources */, 68C3AF257678F6E7BB238C3F /* AppAppearance.swift in Sources */, @@ -8258,6 +8270,7 @@ buildActionMask = 2147483647; files = ( C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */, + A1A16FEF565ED7972591988B /* AccountProviderTests.swift in Sources */, A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */, 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */, 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */, @@ -8267,6 +8280,7 @@ 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */, 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */, EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */, + D7A26828D8715A17A1D52E66 /* AppSettingsTests.swift in Sources */, 3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */, 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */, 5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */, @@ -8450,6 +8464,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3A363617757E6838AC3D15FB /* AccountProvider.swift in Sources */, C6AC34B731F4F853E9D15146 /* AnalyticsConfiguration.swift in Sources */, 585DCA0487A0A6F4E59EF5CA /* AnalyticsConsentState.swift in Sources */, 88887E40301FF0543ED5B59F /* AppAppearance.swift in Sources */, @@ -8521,6 +8536,7 @@ 70394ECD2DCC70741538620D /* AccessibilityIdentifiers.swift in Sources */, 28E8C44DD6E39BEB2A1599C8 /* AccessibilityTestsAppCoordinator.swift in Sources */, 2538350683F421AB620AA950 /* AccessibleLongPress.swift in Sources */, + 03E9750B2F994A79FA1296A1 /* AccountProvider.swift in Sources */, 21215E17C9908F1EBF621BE0 /* ActiveCallTimelineItemView.swift in Sources */, 09E1A358EA4205480AEF4698 /* AdvancedSettingsScreen.swift in Sources */, 34ADC506C929D10B138EBA51 /* AdvancedSettingsScreenCoordinator.swift in Sources */, diff --git a/ElementX/Sources/AppHooks/Hooks/RemoteSettingsHook.swift b/ElementX/Sources/AppHooks/Hooks/RemoteSettingsHook.swift index d9727fe34..7bcb6bfdc 100644 --- a/ElementX/Sources/AppHooks/Hooks/RemoteSettingsHook.swift +++ b/ElementX/Sources/AppHooks/Hooks/RemoteSettingsHook.swift @@ -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(()) diff --git a/ElementX/Sources/Application/Settings/AccountProvider.swift b/ElementX/Sources/Application/Settings/AccountProvider.swift new file mode 100644 index 000000000..b732e0298 --- /dev/null +++ b/ElementX/Sources/Application/Settings/AccountProvider.swift @@ -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 + } + } + } +} diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 6386fe877..bdeeb1d14 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -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 diff --git a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift index a644f2074..9298ca725 100644 --- a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift @@ -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) } } diff --git a/ElementX/Sources/Mocks/ClientFactoryMock.swift b/ElementX/Sources/Mocks/ClientFactoryMock.swift index 3094df788..e7d7d551e 100644 --- a/ElementX/Sources/Mocks/ClientFactoryMock.swift +++ b/ElementX/Sources/Mocks/ClientFactoryMock.swift @@ -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.") } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 791b96af9..6fa2f8597 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -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 diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift index 2aaba760e..13f282dce 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -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) } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift index 765b7406a..e54bdd8d7 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift @@ -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, diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift index 31f940995..a9d9bb53b 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift @@ -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, diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift index e13c9048e..34f6d0949 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift @@ -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? diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift index d6ff42420..6f6d8c558 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift @@ -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) } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift index 6ae0ef046..d0bacfa17 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift @@ -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 diff --git a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenModels.swift b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenModels.swift index ed9e3afd7..caf988f1e 100644 --- a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenModels.swift @@ -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 { diff --git a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift index 2483296b3..31a30d796 100644 --- a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift @@ -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) { diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index 7d05e7687..871e44b6b 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -22,6 +22,9 @@ class AuthenticationService: AuthenticationServiceProtocol { private let appHooks: AppHooks private let homeserverSubject: CurrentValueSubject + /// The currently configured homeserver. + /// + /// **Note:** The value's `accountProvider` will always be `.generic` after calling `configure(for:flow:)`. var homeserver: CurrentValuePublisher { 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 { + func configure(for serverNameOrBaseURL: String, flow: AuthenticationFlow) async -> Result { 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) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index f0ed5c513..ade43ea1c 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -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 { 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 + /// 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 /// Performs login using OAuth for the current homeserver. func urlForOAuthLogin(loginHint: String?) async -> Result /// Asks the SDK to abort an ongoing OAuth login if we didn't get a callback to complete the request with. diff --git a/ElementX/Sources/Services/Client/ClientFactory.swift b/ElementX/Sources/Services/Client/ClientFactory.swift index 5e7553fd5..c7c09fba3 100644 --- a/ElementX/Sources/Services/Client/ClientFactory.swift +++ b/ElementX/Sources/Services/Client/ClientFactory.swift @@ -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) } diff --git a/ElementX/Sources/Services/Client/ClientFactoryProtocol.swift b/ElementX/Sources/Services/Client/ClientFactoryProtocol.swift index e82f20e26..85d4ae1ee 100644 --- a/ElementX/Sources/Services/Client/ClientFactoryProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientFactoryProtocol.swift @@ -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 diff --git a/ElementX/Sources/Services/Client/HomeserverHistoryManager.swift b/ElementX/Sources/Services/Client/HomeserverHistoryManager.swift index b8a14ac87..b1598befe 100644 --- a/ElementX/Sources/Services/Client/HomeserverHistoryManager.swift +++ b/ElementX/Sources/Services/Client/HomeserverHistoryManager.swift @@ -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 } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 0e5bb3de1..7c721067a 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -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, diff --git a/Enterprise b/Enterprise index adca548af..be536ca83 160000 --- a/Enterprise +++ b/Enterprise @@ -1 +1 @@ -Subproject commit adca548afebc930931b5652051d7d970bf9338e4 +Subproject commit be536ca83c346e7b24545b225ddbadd7f7a4f2fc diff --git a/UnitTests/Sources/AccountProviderTests.swift b/UnitTests/Sources/AccountProviderTests.swift new file mode 100644 index 000000000..221df195d --- /dev/null +++ b/UnitTests/Sources/AccountProviderTests.swift @@ -0,0 +1,44 @@ +// +// 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. +// + +@testable import ElementX +import Foundation +import Testing + +struct AccountProviderTests { + @Test + func genericAddressIsSanitized() { + // Lowercased, whitespace trimmed, https:// dropped and trailing slashes removed. + #expect(AccountProvider.generic("Matrix.org").serverNameOrBaseURL == "matrix.org") + #expect(AccountProvider.generic(" matrix.org ").serverNameOrBaseURL == "matrix.org") + #expect(AccountProvider.generic("https://matrix.org").serverNameOrBaseURL == "matrix.org") + #expect(AccountProvider.generic("https://matrix.org/").serverNameOrBaseURL == "matrix.org") + #expect(AccountProvider.generic("HTTPS://Matrix.org///").serverNameOrBaseURL == "matrix.org") + } + + @Test + func genericAddressKeepsHTTPScheme() { + #expect(AccountProvider.generic("http://localhost:8008").serverNameOrBaseURL == "http://localhost:8008", + "The http:// scheme should be preserved as an indicator") + } + + @Test + func managedUsesServerNameWhenPresent() { + let provider = AccountProvider.managed(serverName: "Matrix.org", + baseURL: "https://matrix-client.matrix.org", + canUseServerName: true) + #expect(provider.serverNameOrBaseURL == "matrix.org") + } + + @Test + func managedFallsBackToBaseURLWhenShouldNotUserServerName() { + let provider = AccountProvider.managed(serverName: "Matrix.org", + baseURL: "https://matrix-client.matrix.org", + canUseServerName: false) + #expect(provider.serverNameOrBaseURL == "matrix-client.matrix.org") + } +} diff --git a/UnitTests/Sources/AppSettingsTests.swift b/UnitTests/Sources/AppSettingsTests.swift new file mode 100644 index 000000000..dc2e736ae --- /dev/null +++ b/UnitTests/Sources/AppSettingsTests.swift @@ -0,0 +1,51 @@ +// +// 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. +// + +@testable import ElementX +import Testing + +struct AppSettingsTests { + @Test + func defaultAccountProvider() { + let appSettings = AppSettings.volatile() + #expect(appSettings.defaultAccountProvider == appSettings.accountProviders[0]) + + appSettings.previousServers = ["example.com"] + #expect(appSettings.defaultAccountProvider == .generic("example.com")) + + appSettings.override(allowOtherAccountProviders: false) + #expect(appSettings.defaultAccountProvider == appSettings.accountProviders[0]) + } +} + +// MARK: - Helpers + +private extension AppSettings { + func override(allowOtherAccountProviders: Bool) { + override(accountProviders: accountProviders, + allowOtherAccountProviders: allowOtherAccountProviders, + hideBrandChrome: hideBrandChrome, + pushGatewayBaseURL: pushGatewayBaseURL, + oAuthRedirectURL: oAuthRedirectURL, + oAuthClientURIPath: oAuthClientURIPath, + websiteURL: websiteURL, + logoURL: logoURL, + copyrightURL: copyrightURL, + acceptableUseURL: acceptableUseURL, + privacyURL: privacyURL, + encryptionURL: encryptionURL, + deviceVerificationURL: deviceVerificationURL, + chatBackupDetailsURL: chatBackupDetailsURL, + identityPinningViolationDetailsURL: identityPinningViolationDetailsURL, + historySharingDetailsURL: historySharingDetailsURL, + elementWebHosts: elementWebHosts, + accountProvisioningHost: accountProvisioningHost, + bugReportApplicationID: bugReportApplicationID, + analyticsTermsURL: analyticsTermsURL, + mapTilerConfiguration: mapTilerConfiguration.publisher.value) + } +} diff --git a/UnitTests/Sources/AuthenticationServiceTests.swift b/UnitTests/Sources/AuthenticationServiceTests.swift index eb4a206b8..a12688feb 100644 --- a/UnitTests/Sources/AuthenticationServiceTests.swift +++ b/UnitTests/Sources/AuthenticationServiceTests.swift @@ -21,7 +21,7 @@ struct AuthenticationServiceTests { @Test mutating func passwordLogin() async throws { - try await setup(serverAddress: "example.com") + try await setup(serverNameOrBaseURL: "example.com") switch await service.configure(for: "example.com", flow: .login) { case .success: @@ -65,21 +65,20 @@ struct AuthenticationServiceTests { } @Test - @MainActor mutating func configureRegisterNoSupport() async throws { - let homeserverAddress = "example.com" - try await setup(serverAddress: homeserverAddress) + let serverNameOrBaseURL = "example.com" + try await setup(serverNameOrBaseURL: serverNameOrBaseURL) try await #require(throws: AuthenticationServiceError.registrationNotSupported) { - try await service.configure(for: homeserverAddress, flow: .register).get() + try await service.configure(for: serverNameOrBaseURL, flow: .register).get() } #expect(service.flow == .login) - #expect(service.homeserver.value == .init(address: "matrix.org", loginMode: .unknown)) + #expect(service.homeserver.value == .init(accountProvider: .managed(serverName: "matrix.org", baseURL: "https://matrix-client.matrix.org"), + loginMode: .unknown)) } @Test - @MainActor mutating func classicAppAccountSecretsBundleIsUsed() async throws { // Given an authentication service with an Element Classic account for Alice. try await setup(classicAppAccounts: [.mockAlice]) @@ -96,7 +95,6 @@ struct AuthenticationServiceTests { } @Test - @MainActor mutating func classicAppAccountSecretsBundleIsIgnoredWhenUnavailable() async throws { // Given an authentication service with an Element Classic account for Alice // which isn't configured with any available secrets. @@ -114,7 +112,6 @@ struct AuthenticationServiceTests { } @Test - @MainActor mutating func classicAppAccountSecretsBundleIsIgnoredForDifferentUser() async throws { // Given an authentication service with an Element Classic account for Dan. try await setup(classicAppAccounts: [.mockDan]) @@ -132,13 +129,13 @@ struct AuthenticationServiceTests { // MARK: - Helpers - private mutating func setup(serverAddress: String = "matrix.org", + private mutating func setup(serverNameOrBaseURL: String = "matrix.org", classicAppAccounts: [ClassicAppAccount] = [], availableSecrets: ClassicAppAccount.AvailableSecrets = .complete) async throws { let configuration: ClientFactoryMock.Configuration = .init() let clientFactory = ClientFactoryMock(configuration) - client = configuration.homeserverClients[serverAddress] + client = configuration.homeserverClients[serverNameOrBaseURL] encryption = EncryptionSDKMock() client.encryptionReturnValue = encryption diff --git a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift index c19270261..7d1a41909 100644 --- a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift +++ b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift @@ -50,7 +50,7 @@ final class AuthenticationStartScreenViewModelTests { try await deferred.fulfill() // Then the authentication service should not be used yet. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0) #expect(authenticationService.homeserver.value.loginMode == .unknown) } @@ -69,7 +69,7 @@ final class AuthenticationStartScreenViewModelTests { context.send(viewAction: .login) try await deferred.fulfill() - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "user@company.com") @@ -90,14 +90,14 @@ final class AuthenticationStartScreenViewModelTests { try await deferred.fulfill() // Then a call to configure service should be made. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(authenticationService.homeserver.value.loginMode == .password) } @Test func singleProviderOAuthState() async throws { // Given a view model that for an app that only allows the use of a single provider that supports OAuth. - setAllowedAccountProviders(["company.com"]) + setAllowedAccountProviders([.generic("company.com")]) await setupViewModel() #expect(authenticationService.homeserver.value.loginMode == .unknown) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0) @@ -108,7 +108,7 @@ final class AuthenticationStartScreenViewModelTests { context.send(viewAction: .login) try await deferred.fulfill() - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == nil) @@ -118,7 +118,7 @@ final class AuthenticationStartScreenViewModelTests { @Test func singleProviderPasswordState() async throws { // Given a view model that for an app that only allows the use of a single provider that does not support OAuth. - setAllowedAccountProviders(["company.com"]) + setAllowedAccountProviders([.generic("company.com")]) await setupViewModel(supportsOAuth: false) #expect(authenticationService.homeserver.value.loginMode == .unknown) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0) @@ -130,7 +130,7 @@ final class AuthenticationStartScreenViewModelTests { try await deferred.fulfill() // Then a call to configure service should be made. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(authenticationService.homeserver.value.loginMode == .password) } @@ -153,8 +153,8 @@ final class AuthenticationStartScreenViewModelTests { context.send(viewAction: .continueWithClassic(classicAppAccount)) try await deferred.fulfill() - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.homeserverAddress == "company.com") + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.serverNameOrBaseURL == "company.com") #expect(authenticationService.homeserver.value.loginMode == .oAuth(supportsCreatePrompt: false)) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)") } @@ -177,8 +177,8 @@ final class AuthenticationStartScreenViewModelTests { context.send(viewAction: .continueWithClassic(classicAppAccount)) try await deferred.fulfill() - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.homeserverAddress == "https://matrix.company.com") + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.serverNameOrBaseURL == "https://matrix.company.com") #expect(authenticationService.homeserver.value.loginMode == .oAuth(supportsCreatePrompt: false)) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)") } @@ -214,7 +214,7 @@ final class AuthenticationStartScreenViewModelTests { // Given a view model for an app that only allows a single provider that matches the Classic account's server. let classicAppAccount = makeClassicAppAccount(serverName: "company.com", homeserverURL: "https://matrix.company.com") - setAllowedAccountProviders(["company.com"]) + setAllowedAccountProviders([.generic("company.com")]) await setupViewModel(classicAppAccount: classicAppAccount) // Then the Classic app account should be shown as a welcome-back option. @@ -230,7 +230,7 @@ final class AuthenticationStartScreenViewModelTests { // Given a view model for an app that only allows a single provider that does NOT match the Classic account's server. let classicAppAccount = makeClassicAppAccount(serverName: "other-server.org", homeserverURL: "https://matrix.other-server.org") - setAllowedAccountProviders(["company.com"]) + setAllowedAccountProviders([.generic("company.com")]) await setupViewModel(classicAppAccount: classicAppAccount) // Then the Classic app account should not be shown since the server is not in the allowed providers. @@ -278,7 +278,7 @@ final class AuthenticationStartScreenViewModelTests { supportsOAuth: Bool = true, supportsPasswordLogin: Bool = true, availableSecrets: ClassicAppAccount.AvailableSecrets = .complete) async { - // Manually create a configuration as the default homeserver address setting is immutable. + // Manually create a configuration as the default account provider setting is immutable. client = ClientSDKMock(.init(oAuthLoginURL: supportsOAuth ? "https://account.company.com/authorize" : nil, supportsOAuthCreatePrompt: false, supportsPasswordLogin: supportsPasswordLogin)) @@ -330,7 +330,7 @@ final class AuthenticationStartScreenViewModelTests { accessToken: "accessToken") } - private func setAllowedAccountProviders(_ providers: [String]) { + private func setAllowedAccountProviders(_ providers: [AccountProvider]) { appSettings.override(accountProviders: providers, allowOtherAccountProviders: false, hideBrandChrome: false, diff --git a/UnitTests/Sources/LoginScreenViewModelTests.swift b/UnitTests/Sources/LoginScreenViewModelTests.swift index 5eb7816c9..36261131d 100644 --- a/UnitTests/Sources/LoginScreenViewModelTests.swift +++ b/UnitTests/Sources/LoginScreenViewModelTests.swift @@ -226,7 +226,7 @@ struct LoginScreenViewModelTests { // MARK: - Helpers - private mutating func setupViewModel(homeserverAddress: String = "example.com", loginHint: String? = nil) async { + private mutating func setupViewModel(serverNameOrBaseURL: String = "example.com", loginHint: String? = nil) async { let appSettings = AppSettings.volatile() clientFactory = ClientFactoryMock(.init()) @@ -238,7 +238,7 @@ struct LoginScreenViewModelTests { appHooks: AppHooks()) guard case .success = await service - .configure(for: homeserverAddress, flow: .login) else { + .configure(for: serverNameOrBaseURL, flow: .login) else { Issue.record("A valid server should be configured for the test.") return } diff --git a/UnitTests/Sources/NotificationContentBuilderTests.swift b/UnitTests/Sources/NotificationContentBuilderTests.swift index ce861fdb2..d7278a459 100644 --- a/UnitTests/Sources/NotificationContentBuilderTests.swift +++ b/UnitTests/Sources/NotificationContentBuilderTests.swift @@ -17,14 +17,14 @@ nonisolated struct NotificationContentBuilderTests { var mediaProvider: MediaProviderMock var notificationContent: UNMutableNotificationContent - init() { + init() async { notificationContent = .init() let stringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), style: .plain) mediaProvider = MediaProviderMock(.init()) - notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: stringBuilder, - notificationSoundName: UNNotificationSoundName("message.caf"), - userSession: NSEUserSessionMock(.init())) + notificationContentBuilder = await NotificationContentBuilder(messageEventStringBuilder: stringBuilder, + notificationSoundName: UNNotificationSoundName("message.caf"), + userSession: NSEUserSessionMock(.init())) } @Test diff --git a/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift index f445916ca..7251d0f59 100644 --- a/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift @@ -14,7 +14,7 @@ import Testing @MainActor struct ServerSelectionScreenViewModelTests { private static let passwordOnlyServer = "example.com" // password only, no OAuth - private static let pickerProviders = ["matrix.org", "beta.matrix.org", passwordOnlyServer] + private static let pickerProviders: [String] = ["matrix.org", "beta.matrix.org", passwordOnlyServer] var appSettings: AppSettings! var client: ClientSDKMock! @@ -31,16 +31,16 @@ struct ServerSelectionScreenViewModelTests { // Given a view model for login. try setup(authenticationFlow: .login) #expect(service.homeserver.value.loginMode == .unknown) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) // When selecting matrix.org. - context.homeserverAddress = "matrix.org" + context.serverNameOrBaseURL = "matrix.org" let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOAuth } context.send(viewAction: .confirm) try await deferred.fulfill() // Then selection should succeed. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(service.homeserver.value == .mockMatrixDotOrg) } @@ -51,13 +51,13 @@ struct ServerSelectionScreenViewModelTests { #expect(service.homeserver.value.loginMode == .unknown) // When entering a Matrix ID instead of an account provider. - context.homeserverAddress = "@alice:matrix.org" + context.serverNameOrBaseURL = "@alice:matrix.org" let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOAuth } context.send(viewAction: .confirm) try await deferred.fulfill() // Then the homeserver from the ID should be used. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.homeserverAddress == "matrix.org") + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.serverNameOrBaseURL == "matrix.org") #expect(service.homeserver.value == .mockMatrixDotOrg) } @@ -66,17 +66,17 @@ struct ServerSelectionScreenViewModelTests { // Given a view model for login. try setup(authenticationFlow: .login) #expect(service.homeserver.value.loginMode == .unknown) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) #expect(context.alertInfo == nil) // When selecting a server that doesn't support login. - context.homeserverAddress = "server.net" + context.serverNameOrBaseURL = "server.net" let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil } context.send(viewAction: .confirm) try await deferred.fulfill() // Then selection should fail with an alert about not supporting registration. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(context.alertInfo?.id == .loginAlert) } @@ -85,16 +85,16 @@ struct ServerSelectionScreenViewModelTests { // Given a view model for registration. try setup(authenticationFlow: .register) #expect(service.homeserver.value.loginMode == .unknown) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) // When selecting matrix.org. - context.homeserverAddress = "matrix.org" + context.serverNameOrBaseURL = "matrix.org" let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOAuth } context.send(viewAction: .confirm) try await deferred.fulfill() // Then selection should succeed. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(service.homeserver.value == .mockMatrixDotOrg) } @@ -103,17 +103,17 @@ struct ServerSelectionScreenViewModelTests { // Given a view model for registration. try setup(authenticationFlow: .register) #expect(service.homeserver.value.loginMode == .unknown) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) #expect(context.alertInfo == nil) // When selecting a server that doesn't support registration. - context.homeserverAddress = "example.com" + context.serverNameOrBaseURL = "example.com" let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil } context.send(viewAction: .confirm) try await deferred.fulfill() // Then selection should fail with an alert about not supporting registration. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(context.alertInfo?.id == .registrationAlert) } @@ -122,17 +122,17 @@ struct ServerSelectionScreenViewModelTests { // Given a view model for login. try setup(authenticationFlow: .login) #expect(service.homeserver.value.loginMode == .unknown) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) #expect(context.alertInfo == nil) // When selecting a server that requires Element Pro - context.homeserverAddress = "secure.gov" + context.serverNameOrBaseURL = "secure.gov" let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil } context.send(viewAction: .confirm) try await deferred.fulfill() // Then selection should fail with an alert telling the user to download Element Pro. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(context.alertInfo?.id == .elementProAlert) } @@ -147,7 +147,7 @@ struct ServerSelectionScreenViewModelTests { // When attempting to discover an invalid server var deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { $0 } - context.homeserverAddress = "idontexist" + context.serverNameOrBaseURL = "idontexist" context.send(viewAction: .confirm) try await deferred.fulfill() @@ -159,7 +159,7 @@ struct ServerSelectionScreenViewModelTests { // And when clearing the error. deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { !$0 } - context.homeserverAddress = "" + context.serverNameOrBaseURL = "" context.send(viewAction: .clearFooterError) try await deferred.fulfill() @@ -175,9 +175,9 @@ struct ServerSelectionScreenViewModelTests { mutating func userInputPasswordLoginWithoutConfiguration() async throws { // Given a view model for login using a service that hasn't been configured against a server that doesn't support OAuth. try setup(authenticationFlow: .login) - context.homeserverAddress = Self.passwordOnlyServer + context.serverNameOrBaseURL = Self.passwordOnlyServer #expect(service.homeserver.value.loginMode == .unknown) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) // When confirming from the server selection screen. let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword } @@ -185,7 +185,7 @@ struct ServerSelectionScreenViewModelTests { try await deferred.fulfill() // Then the service should be configured but no OAuth URL fetched. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(service.homeserver.value.loginMode == .password) } @@ -193,13 +193,13 @@ struct ServerSelectionScreenViewModelTests { mutating func userInputPasswordLoginAfterConfiguration() async throws { // Given a view model for login using a service that has already been configured against a server that doesn't support OAuth. try setup(authenticationFlow: .login) - context.homeserverAddress = Self.passwordOnlyServer - guard case .success = await service.configure(for: context.homeserverAddress, flow: .login) else { + context.serverNameOrBaseURL = Self.passwordOnlyServer + guard case .success = await service.configure(for: context.serverNameOrBaseURL, flow: .login) else { Issue.record("The configuration should succeed.") return } #expect(service.homeserver.value.loginMode == .password) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) // When confirming from the server selection screen. let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword } @@ -207,7 +207,7 @@ struct ServerSelectionScreenViewModelTests { try await deferred.fulfill() // Then the service should be re-configured but no OAuth URL fetched. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2) } // MARK: - Picker mode @@ -218,7 +218,7 @@ struct ServerSelectionScreenViewModelTests { try setup(authenticationFlow: .login, mode: .picker(Self.pickerProviders)) #expect(service.homeserver.value.loginMode == .unknown) #expect(context.viewState.mode == .picker(Self.pickerProviders)) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0) // When confirming from the picker. @@ -227,7 +227,7 @@ struct ServerSelectionScreenViewModelTests { try await deferred.fulfill() // Then the service should be configured and the OAuth URL fetched. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1) #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent) #expect(service.homeserver.value.loginMode == .oAuth(supportsCreatePrompt: true)) @@ -237,9 +237,9 @@ struct ServerSelectionScreenViewModelTests { mutating func pickerForPasswordLoginWithoutConfiguration() async throws { // Given a view model for login using a service that hasn't been configured against a server that doesn't support OAuth. try setup(authenticationFlow: .login, mode: .picker(Self.pickerProviders)) - context.homeserverAddress = Self.passwordOnlyServer + context.serverNameOrBaseURL = Self.passwordOnlyServer #expect(service.homeserver.value.loginMode == .unknown) - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0) // When confirming from the picker. let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword } @@ -247,7 +247,7 @@ struct ServerSelectionScreenViewModelTests { try await deferred.fulfill() // Then the service should be configured but no OAuth URL fetched. - #expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) #expect(service.homeserver.value.loginMode == .password) } @@ -258,7 +258,7 @@ struct ServerSelectionScreenViewModelTests { // Given a view model with a previous server in history. try setup(authenticationFlow: .login) appSettings.previousServers = ["myserver.com"] - context.homeserverAddress = "" + context.serverNameOrBaseURL = "" let textField = UITextField() context.send(viewAction: .updateTextField(textField)) @@ -268,14 +268,14 @@ struct ServerSelectionScreenViewModelTests { let typed = typedSoFar + nextCharacter let expectedAddress = "myserver.com" textField.text = typedSoFar - let deferred = deferFulfillment(context.observe(\.homeserverAddress)) { $0 == expectedAddress } + let deferred = deferFulfillment(context.observe(\.serverNameOrBaseURL)) { $0 == expectedAddress } _ = textField.delegate?.textField?(textField, shouldChangeCharactersIn: NSRange(location: typedSoFar.count, length: 0), replacementString: nextCharacter) try await deferred.fulfill() // Then the address should be completed and the appended portion selected. let selectionStart = expectedAddress.index(expectedAddress.startIndex, offsetBy: typed.count) - #expect(context.homeserverAddress == expectedAddress) - #expect(context.homeserverSelection == TextSelection(range: selectionStart..