diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 008050994d..fc5cba257d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -829,7 +829,6 @@ B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; - B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */; }; B773ACD8881DB18E876D950C /* WaveformSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94028A227645FA880B966211 /* WaveformSource.swift */; }; B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; }; B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; @@ -2141,7 +2140,6 @@ D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelProtocol.swift; sourceTree = ""; }; D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = ""; }; - D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = ""; }; D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = ""; }; D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInviteCell.swift; sourceTree = ""; }; @@ -2815,7 +2813,6 @@ 2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */ = { isa = PBXGroup; children = ( - D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */, BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */, 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */, E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */, @@ -6593,7 +6590,6 @@ C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */, B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */, 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */, - B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */, AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */, F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */, EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */, diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift index 811a7fa676..352bb7e28a 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift @@ -72,6 +72,8 @@ enum ServerConfirmationScreenAlert: Hashable { case invalidWellKnown(String) /// An alert that allows the user to learn about sliding sync. case slidingSync + /// An alert that informs the user that login isn't supported. + case login /// An alert that informs the user that registration isn't supported. case registration /// An unknown error has occurred. diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift index 29563103da..127bf29ed2 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift @@ -82,6 +82,8 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, displayError(.invalidWellKnown(error)) case .slidingSyncNotAvailable: displayError(.slidingSync) + case .loginNotSupported: + displayError(.login) case .registrationNotSupported: displayError(.registration) default: @@ -117,9 +119,13 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, message: L10n.screenChangeServerErrorNoSlidingSyncMessage, primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), secondaryButton: .init(title: L10n.actionCancel, action: nil)) + case .login: + state.bindings.alertInfo = AlertInfo(id: .login, + title: L10n.commonServerNotSupported, + message: L10n.screenLoginErrorUnsupportedAuthentication) case .registration: state.bindings.alertInfo = AlertInfo(id: .registration, - title: L10n.errorUnknown, + title: L10n.commonServerNotSupported, message: L10n.errorAccountCreationNotPossible) case .unknownError: state.bindings.alertInfo = AlertInfo(id: .unknownError) diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/MockServerSelectionScreenState.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/MockServerSelectionScreenState.swift deleted file mode 100644 index c8393a4bdb..0000000000 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/MockServerSelectionScreenState.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import SwiftUI - -enum MockServerSelectionScreenState: CaseIterable { - case matrix - case emptyAddress - case invalidAddress - - /// Generate the view struct for the screen state. - @MainActor var viewModel: ServerSelectionScreenViewModel { - switch self { - case .matrix: - return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) - - case .emptyAddress: - return ServerSelectionScreenViewModel(homeserverAddress: "", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) - case .invalidAddress: - let viewModel = ServerSelectionScreenViewModel(homeserverAddress: "thisisbad", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) - viewModel.displayError(.footerMessage(L10n.errorUnknown)) - return viewModel - } - } -} diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift index 71e78682e1..889b5ef62a 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift @@ -37,8 +37,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { init(parameters: ServerSelectionScreenCoordinatorParameters) { self.parameters = parameters - viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address, - slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL) + viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService, + authenticationFlow: parameters.authenticationFlow, + slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL, + userIndicatorController: parameters.userIndicatorController) userIndicatorController = parameters.userIndicatorController } @@ -50,8 +52,8 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .confirm(let homeserverAddress): - self.useHomeserver(homeserverAddress) + case .updated: + actionsSubject.send(.updated) case .dismiss: actionsSubject.send(.dismiss) } @@ -60,56 +62,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { } func stop() { - stopLoading() + parameters.userIndicatorController.retractAllIndicators() } func toPresentable() -> AnyView { AnyView(ServerSelectionScreen(context: viewModel.context)) } - - // MARK: - Private - - private func startLoading(label: String = L10n.commonLoading) { - userIndicatorController.submitIndicator(UserIndicator(type: .modal, - title: label, - persistent: true)) - } - - private func stopLoading() { - userIndicatorController.retractAllIndicators() - } - - /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible. - private func useHomeserver(_ homeserverAddress: String) { - startLoading() - - Task { - switch await authenticationService.configure(for: homeserverAddress, flow: parameters.authenticationFlow) { - case .success: - MXLog.info("Selected homeserver: \(homeserverAddress)") - actionsSubject.send(.updated) - stopLoading() - case .failure(let error): - MXLog.info("Invalid homeserver: \(homeserverAddress)") - stopLoading() - handleError(error) - } - } - } - - /// Processes an error to either update the flow or display it to the user. - private func handleError(_ error: AuthenticationServiceError) { - switch error { - case .invalidServer, .invalidHomeserverAddress: - viewModel.displayError(.footerMessage(L10n.screenChangeServerErrorInvalidHomeserver)) - case .invalidWellKnown(let error): - viewModel.displayError(.invalidWellKnownAlert(error)) - case .slidingSyncNotAvailable: - viewModel.displayError(.slidingSyncAlert) - case .registrationNotSupported: - viewModel.displayError(.registrationAlert) // TODO: [DOUG] Test me! - default: - viewModel.displayError(.footerMessage(L10n.errorUnknown)) - } - } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift index ae11444bc8..db72a3608b 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift @@ -8,8 +8,8 @@ import Foundation enum ServerSelectionScreenViewModelAction { - /// The user would like to use the homeserver at the given address. - case confirm(homeserverAddress: String) + /// The homeserver selection has been updated. + case updated /// Dismiss the view without using the entered address. case dismiss } @@ -74,6 +74,8 @@ enum ServerSelectionScreenErrorType: Hashable { case invalidWellKnownAlert(String) /// An alert that allows the user to learn about sliding sync. case slidingSyncAlert + /// An alert that informs the user that login isn't supported. + case loginAlert /// An alert that informs the user that registration isn't supported. case registrationAlert } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift index fea0915420..a13227b0b7 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift @@ -11,7 +11,10 @@ import SwiftUI typealias ServerSelectionScreenViewModelType = StateStoreViewModel class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, ServerSelectionScreenViewModelProtocol { + private let authenticationService: AuthenticationServiceProtocol + private let authenticationFlow: AuthenticationFlow private let slidingSyncLearnMoreURL: URL + private let userIndicatorController: UserIndicatorControllerProtocol private var actionsSubject: PassthroughSubject = .init() @@ -19,18 +22,23 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server actionsSubject.eraseToAnyPublisher() } - init(homeserverAddress: String, slidingSyncLearnMoreURL: URL) { + init(authenticationService: AuthenticationServiceProtocol, + authenticationFlow: AuthenticationFlow, + slidingSyncLearnMoreURL: URL, + userIndicatorController: UserIndicatorControllerProtocol) { + self.authenticationService = authenticationService + self.authenticationFlow = authenticationFlow self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL - let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress) + self.userIndicatorController = userIndicatorController - super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, - bindings: bindings)) + let bindings = ServerSelectionScreenBindings(homeserverAddress: authenticationService.homeserver.value.address) + super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, bindings: bindings)) } override func process(viewAction: ServerSelectionScreenViewAction) { switch viewAction { case .confirm: - actionsSubject.send(.confirm(homeserverAddress: state.bindings.homeserverAddress)) + configureHomeserver() case .dismiss: actionsSubject.send(.dismiss) case .clearFooterError: @@ -38,31 +46,72 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server } } - func displayError(_ type: ServerSelectionScreenErrorType) { - switch type { - case .footerMessage(let message): - withElementAnimation { - state.footerErrorMessage = message + // MARK: - Private + + /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible. + private func configureHomeserver() { + let homeserverAddress = state.bindings.homeserverAddress + startLoading() + + Task { + switch await authenticationService.configure(for: homeserverAddress, flow: authenticationFlow) { + case .success: + MXLog.info("Selected homeserver: \(homeserverAddress)") + actionsSubject.send(.updated) + stopLoading() + case .failure(let error): + MXLog.info("Invalid homeserver: \(homeserverAddress)") + stopLoading() + handleError(error) } - case .invalidWellKnownAlert(let error): + } + } + + private func startLoading(label: String = L10n.commonLoading) { + userIndicatorController.submitIndicator(UserIndicator(type: .modal, + title: label, + persistent: true)) + } + + private func stopLoading() { + userIndicatorController.retractAllIndicators() + } + + /// Processes an error to either update the flow or display it to the user. + private func handleError(_ error: AuthenticationServiceError) { + switch error { + case .invalidServer, .invalidHomeserverAddress: + showFooterMessage(L10n.screenChangeServerErrorInvalidHomeserver) + case .invalidWellKnown(let error): state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error), title: L10n.commonServerNotSupported, message: L10n.screenChangeServerErrorInvalidWellKnown(error)) - case .slidingSyncAlert: + case .slidingSyncNotAvailable: let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) } state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, title: L10n.commonServerNotSupported, message: L10n.screenChangeServerErrorNoSlidingSyncMessage, primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), secondaryButton: .init(title: L10n.actionCancel, action: nil)) - case .registrationAlert: + case .loginNotSupported: + state.bindings.alertInfo = AlertInfo(id: .loginAlert, + title: L10n.commonServerNotSupported, + message: L10n.screenLoginErrorUnsupportedAuthentication) + case .registrationNotSupported: state.bindings.alertInfo = AlertInfo(id: .registrationAlert, - title: L10n.errorUnknown, + title: L10n.commonServerNotSupported, message: L10n.errorAccountCreationNotPossible) + default: + showFooterMessage(L10n.errorUnknown) } } - // MARK: - Private + /// Set a new error message to be shown in the text field footer. + private func showFooterMessage(_ message: String) { + withElementAnimation { + state.footerErrorMessage = message + } + } /// Clear any errors shown in the text field footer. private func clearFooterError() { diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift index 320c0842a6..c8f748d60d 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift @@ -11,7 +11,4 @@ import Combine protocol ServerSelectionScreenViewModelProtocol { var actions: AnyPublisher { get } var context: ServerSelectionScreenViewModelType.Context { get } - - /// Displays an error to the user. - func displayError(_ type: ServerSelectionScreenErrorType) } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift index 03bb915504..122b34a171 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift @@ -91,11 +91,38 @@ struct ServerSelectionScreen: View { // MARK: - Previews struct ServerSelection_Previews: PreviewProvider, TestablePreview { + static let matrixViewModel = makeViewModel(for: "https://matrix.org") + static let emptyViewModel = makeViewModel(for: "") + static let invalidViewModel = makeViewModel(for: "thisisbad") + static var previews: some View { - ForEach(MockServerSelectionScreenState.allCases, id: \.self) { state in - NavigationStack { - ServerSelectionScreen(context: state.viewModel.context) - } + NavigationStack { + ServerSelectionScreen(context: matrixViewModel.context) + } + + NavigationStack { + ServerSelectionScreen(context: emptyViewModel.context) + } + + NavigationStack { + ServerSelectionScreen(context: invalidViewModel.context) + } + .snapshotPreferences(delay: 0.25) + } + + static func makeViewModel(for homeserverAddress: String) -> ServerSelectionScreenViewModel { + let authenticationService = AuthenticationService.mock + + let slidingSyncLearnMoreURL = ServiceLocator.shared.settings.slidingSyncLearnMoreURL + + let viewModel = ServerSelectionScreenViewModel(authenticationService: authenticationService, + authenticationFlow: .login, + slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + viewModel.context.homeserverAddress = homeserverAddress + if homeserverAddress == "thisisbad" { + viewModel.context.send(viewAction: .confirm) } + return viewModel } } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index 66dbacb2ae..64f5b91f78 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -65,6 +65,9 @@ class AuthenticationService: AuthenticationServiceProtocol { case .failure: nil } + if flow == .login, homeserver.loginMode == .unsupported { + return .failure(.loginNotSupported) + } if flow == .register, !homeserver.supportsRegistration { return .failure(.registrationNotSupported) } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index 3efa258e90..e762e27cb8 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -24,6 +24,7 @@ enum AuthenticationServiceError: Error, Equatable { case invalidHomeserverAddress case invalidWellKnown(String) case slidingSyncNotAvailable + case loginNotSupported case registrationNotSupported case accountDeactivated case failedLoggingIn diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-en-GB.3.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-en-GB.3.png index 8a65288344..49b86131c9 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-en-GB.3.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-en-GB.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:667d6afdec1cad4db4439278981efc6cafb8c4bb52a28ed951120da64156481c -size 106217 +oid sha256:b24a1036fe61c64214c929d53d409723bf388191205d4392759009680b451ad3 +size 121094 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-pseudo.3.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-pseudo.3.png index ac2cd11120..8014ba2261 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-pseudo.3.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPad-pseudo.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:028f43f14f8ad6f5a7eef82c6bcb01779ddec20b6cda44c405ecd874bc3ed35d -size 115393 +oid sha256:8048d1dd4b99792cd4c68567499cfe3aba40a4e3b0697eda3d1b67a115d684f6 +size 145736 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-en-GB.3.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-en-GB.3.png index e1be2f1285..26e485e145 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-en-GB.3.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-en-GB.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04d2b178fa9a746c0ddef74e83b100011267ba40e98dbcd5ed8d75ec599b89f6 -size 60409 +oid sha256:d6b890b433313f7398c38f940ba1db616b89c563c5ba35e6378b0a4863b9c6b5 +size 75379 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-pseudo.3.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-pseudo.3.png index 4cf24cefd3..45f0a77eed 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-pseudo.3.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_serverSelection-iPhone-16-pseudo.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d05997731f755f45834075ac7dd62cc8b18b94b65354ccd4fb6c198ed91bac1 -size 73627 +oid sha256:1fe38dada4e2201f011443eda61791124e33e5423a049ea4f9b7c942c3405b1b +size 103829 diff --git a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift index 9f9d3148d1..4a551cdb64 100644 --- a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift @@ -89,7 +89,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase { func testRegistrationNotSupportedAlert() async throws { // Given a view model for registration using a service that hasn't been configured and the default server doesn't support registration. - setupViewModel(authenticationFlow: .register, elementWellKnown: false) + setupViewModel(authenticationFlow: .register, supportsRegistrationHelper: false) XCTAssertEqual(service.homeserver.value.loginMode, .unknown) XCTAssertFalse(service.homeserver.value.supportsRegistration) XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) @@ -105,10 +105,34 @@ class ServerConfirmationScreenViewModelTests: XCTestCase { XCTAssertEqual(context.alertInfo?.id, .registration) } + func testLoginNotSupportedAlert() async throws { + // Given a view model for login using a service that hasn't been configured and the default server doesn't support login. + setupViewModel(authenticationFlow: .login, supportsRegistrationHelper: false, supportsPasswordLogin: false) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + XCTAssertNil(context.alertInfo) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the configuration should fail with an alert about not supporting login. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .login) + } + // MARK: - Helpers - private func setupViewModel(authenticationFlow: AuthenticationFlow, elementWellKnown: Bool = true) { - let client = ClientSDKMock(configuration: elementWellKnown ? .init() : .init(elementWellKnown: "")) + private func setupViewModel(authenticationFlow: AuthenticationFlow, supportsRegistrationHelper: Bool = true, supportsPasswordLogin: Bool = true) { + // Manually create a configuration as the default homeserver address setting is immutable. + let clientConfiguration: ClientSDKMock.Configuration = if supportsRegistrationHelper { + .init(supportsPasswordLogin: supportsPasswordLogin) + } else { + .init(supportsPasswordLogin: supportsPasswordLogin, elementWellKnown: "") + } + let client = ClientSDKMock(configuration: clientConfiguration) let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client], qrCodeClient: client) diff --git a/UnitTests/Sources/ServerSelectionViewModelTests.swift b/UnitTests/Sources/ServerSelectionViewModelTests.swift index 7808b62249..fc6411102c 100644 --- a/UnitTests/Sources/ServerSelectionViewModelTests.swift +++ b/UnitTests/Sources/ServerSelectionViewModelTests.swift @@ -11,38 +11,131 @@ import XCTest @MainActor class ServerSelectionViewModelTests: XCTestCase { + var clientBuilderFactory: AuthenticationClientBuilderFactoryMock! + var service: AuthenticationServiceProtocol! + var viewModel: ServerSelectionScreenViewModelProtocol! - var context: ServerSelectionScreenViewModelType.Context! + var context: ServerSelectionScreenViewModelType.Context { viewModel.context } - @MainActor override func setUp() { - viewModel = ServerSelectionScreenViewModel(homeserverAddress: "", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) - context = viewModel.context + func testSelectForLogin() async throws { + // Given a view model for login. + setupViewModel(authenticationFlow: .login) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When selecting matrix.org. + context.homeserverAddress = "matrix.org" + let deferred = deferFulfillment(viewModel.actions) { $0 == .updated } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should succeed. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) } - - func testErrorMessage() async throws { + + func testLoginNotSupportedAlert() async throws { + // Given a view model for login. + setupViewModel(authenticationFlow: .login) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + XCTAssertNil(context.alertInfo) + + // When selecting a server that doesn't support login. + context.homeserverAddress = "server.net" + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should fail with an alert about not supporting registration. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .loginAlert) + } + + func testSelectForRegistration() async throws { + // Given a view model for registration. + setupViewModel(authenticationFlow: .register) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When selecting matrix.org. + context.homeserverAddress = "matrix.org" + let deferred = deferFulfillment(viewModel.actions) { $0 == .updated } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should succeed. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) + } + + func testRegistrationNotSupportedAlert() async throws { + // Given a view model for registration. + setupViewModel(authenticationFlow: .register) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + XCTAssertNil(context.alertInfo) + + // When selecting a server that doesn't support registration. + context.homeserverAddress = "example.com" + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should fail with an alert about not supporting registration. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .registrationAlert) + } + + func testInvalidServer() async throws { // Given a new instance of the view model. + setupViewModel(authenticationFlow: .login) + XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error message for a new view model.") XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.") XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), "The standard footer message should be shown.") - // When an error occurs. - let message = "Unable to contact server." - viewModel.displayError(.footerMessage(message)) + // When attempting to discover an invalid server + var deferred = deferFulfillment(context.$viewState) { $0.isShowingFooterError } + context.homeserverAddress = "idontexist" + context.send(viewAction: .confirm) + try await deferred.fulfill() // Then the footer should now be showing an error. - XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.") - XCTAssertEqual(String(context.viewState.footerMessage.characters), message, "The error message should be shown.") + XCTAssertTrue(context.viewState.isShowingFooterError, "The error message should be stored.") + XCTAssertNotNil(context.viewState.footerErrorMessage, "The error message should be stored.") + XCTAssertNotEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), + "The error message should be shown.") // And when clearing the error. + deferred = deferFulfillment(context.$viewState) { !$0.isShowingFooterError } + context.homeserverAddress = "" context.send(viewAction: .clearFooterError) - - // Wait for the action to spawn a Task. - await Task.yield() + try await deferred.fulfill() // Then the error message should now be removed. XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.") XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), "The standard footer message should be shown again.") } + + // MARK: - Helpers + + private func setupViewModel(authenticationFlow: AuthenticationFlow) { + clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init()) + service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), + encryptionKeyProvider: EncryptionKeyProvider(), + clientBuilderFactory: clientBuilderFactory, + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + + viewModel = ServerSelectionScreenViewModel(authenticationService: service, + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + } }