mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Move the core logic in LoginScreenCoordinator into the ViewModel. (#3348)
This commit is contained in:
parent
5f4c2890f6
commit
268d9f7479
@ -144,7 +144,6 @@
|
||||
1D623953F970D11F6F38499C /* AppLockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B95BB98649B8E773D6790 /* AppLockService.swift */; };
|
||||
1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; };
|
||||
1DC227816777A2F3A19657E5 /* RoomDirectorySearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */; };
|
||||
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; };
|
||||
1F3232BD368DF430AB433907 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; };
|
||||
1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; };
|
||||
1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; };
|
||||
@ -463,6 +462,7 @@
|
||||
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; };
|
||||
67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; };
|
||||
67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */; };
|
||||
67ECD32538F6DAFE38A623F9 /* ServerSelectionScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */; };
|
||||
67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C713D124FE915ABF47A6B7 /* TimelineView.swift */; };
|
||||
6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */; };
|
||||
68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; };
|
||||
@ -513,6 +513,7 @@
|
||||
73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; };
|
||||
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; };
|
||||
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
|
||||
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; };
|
||||
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; };
|
||||
748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; };
|
||||
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; };
|
||||
@ -668,7 +669,6 @@
|
||||
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; };
|
||||
934051B17A884AB0635DF81B /* BlockedUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */; };
|
||||
937985546F708339711ECDFC /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85666E40F7E817809B4FD787 /* ComposerToolbar.swift */; };
|
||||
93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; };
|
||||
93A549135E6C027A0D823BFE /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 593FBBF394712F2963E98A0B /* DTCoreText */; };
|
||||
93AC1E8418D8C827671FB3A9 /* IdentityConfirmedScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595EC503DA5517BBE6D39406 /* IdentityConfirmedScreenCoordinator.swift */; };
|
||||
93BA4A81B6D893271101F9F0 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; };
|
||||
@ -1607,6 +1607,7 @@
|
||||
5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = "<group>"; };
|
||||
5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = "<group>"; };
|
||||
5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
@ -1888,7 +1889,6 @@
|
||||
A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||
A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = "<group>"; };
|
||||
A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = "<group>"; };
|
||||
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@ -2186,6 +2186,7 @@
|
||||
E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = "<group>"; };
|
||||
E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; };
|
||||
E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = "<group>"; };
|
||||
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||
E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; };
|
||||
@ -2233,7 +2234,6 @@
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
||||
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
|
||||
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
|
||||
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
|
||||
EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = "<group>"; };
|
||||
@ -3828,7 +3828,7 @@
|
||||
6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */,
|
||||
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
|
||||
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
|
||||
A05707BF550D770168A406DB /* LoginViewModelTests.swift */,
|
||||
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */,
|
||||
376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */,
|
||||
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */,
|
||||
2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */,
|
||||
@ -3870,7 +3870,7 @@
|
||||
40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */,
|
||||
277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */,
|
||||
F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */,
|
||||
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */,
|
||||
E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */,
|
||||
0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */,
|
||||
A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */,
|
||||
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */,
|
||||
@ -6122,7 +6122,7 @@
|
||||
8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */,
|
||||
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
|
||||
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
|
||||
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */,
|
||||
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */,
|
||||
77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */,
|
||||
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */,
|
||||
4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */,
|
||||
@ -6171,7 +6171,7 @@
|
||||
1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */,
|
||||
53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */,
|
||||
89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */,
|
||||
93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */,
|
||||
67ECD32538F6DAFE38A623F9 /* ServerSelectionScreenViewModelTests.swift in Sources */,
|
||||
CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */,
|
||||
86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */,
|
||||
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */,
|
||||
|
@ -244,8 +244,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func showLoginScreen() {
|
||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
analytics: analytics,
|
||||
userIndicatorController: userIndicatorController)
|
||||
slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: userIndicatorController,
|
||||
analytics: analytics)
|
||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions
|
||||
|
@ -20,10 +20,8 @@ enum LoginMode: Equatable {
|
||||
|
||||
var supportsOIDCFlow: Bool {
|
||||
switch self {
|
||||
case .oidc:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
case .oidc: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ import SwiftUI
|
||||
struct LoginScreenCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
|
||||
let analytics: AnalyticsService
|
||||
let slidingSyncLearnMoreURL: URL
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
enum LoginScreenCoordinatorAction {
|
||||
@ -42,8 +42,10 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: LoginScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver.value,
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
|
||||
viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService,
|
||||
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
analytics: parameters.analytics)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@ -54,119 +56,20 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .parseUsername(let username):
|
||||
parseUsername(username)
|
||||
case .forgotPassword:
|
||||
showForgotPasswordScreen()
|
||||
case .login(let username, let password):
|
||||
login(username: username, password: password)
|
||||
case .configuredForOIDC:
|
||||
actionsSubject.send(.configuredForOIDC)
|
||||
case .signedIn(let userSession):
|
||||
actionsSubject.send(.signedIn(userSession))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
viewModel.stopLoading()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(LoginScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(LoginScreenCoordinatorAction.self)-Loading"
|
||||
|
||||
private func startLoading(isInteractionBlocking: Bool) {
|
||||
if isInteractionBlocking {
|
||||
parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
} else {
|
||||
viewModel.update(isLoading: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopLoading() {
|
||||
viewModel.update(isLoading: false)
|
||||
parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
MXLog.info("Error occurred: \(error)")
|
||||
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
viewModel.displayError(.alert(L10n.screenLoginErrorInvalidCredentials))
|
||||
case .accountDeactivated:
|
||||
viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount))
|
||||
case .invalidWellKnown(let error):
|
||||
viewModel.displayError(.invalidWellKnownAlert(error))
|
||||
case .slidingSyncNotAvailable:
|
||||
viewModel.displayError(.slidingSyncAlert)
|
||||
case .sessionTokenRefreshNotSupported:
|
||||
viewModel.displayError(.refreshTokenAlert)
|
||||
default:
|
||||
viewModel.displayError(.alert(L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests the authentication coordinator to log in using the specified credentials.
|
||||
private func login(username: String, password: String) {
|
||||
MXLog.info("Starting login with password.")
|
||||
startLoading(isInteractionBlocking: true)
|
||||
|
||||
Task {
|
||||
parameters.analytics.signpost.beginLogin()
|
||||
switch await authenticationService.login(username: username,
|
||||
password: password,
|
||||
initialDeviceName: UIDevice.current.initialDeviceName,
|
||||
deviceID: nil) {
|
||||
case .success(let userSession):
|
||||
actionsSubject.send(.signedIn(userSession))
|
||||
parameters.analytics.signpost.endLogin()
|
||||
stopLoading()
|
||||
case .failure(let error):
|
||||
stopLoading()
|
||||
parameters.analytics.signpost.endLogin()
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the specified username and looks up the homeserver when a Matrix ID is entered.
|
||||
private func parseUsername(_ username: String) {
|
||||
guard MatrixEntityRegex.isMatrixUserIdentifier(username) else { return }
|
||||
|
||||
let homeserverDomain = String(username.split(separator: ":")[1])
|
||||
|
||||
startLoading(isInteractionBlocking: false)
|
||||
|
||||
Task {
|
||||
switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
|
||||
case .success:
|
||||
stopLoading()
|
||||
if authenticationService.homeserver.value.loginMode == .oidc {
|
||||
actionsSubject.send(.configuredForOIDC)
|
||||
} else {
|
||||
updateViewModel()
|
||||
}
|
||||
case .failure(let error):
|
||||
stopLoading()
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the view model with a different homeserver.
|
||||
private func updateViewModel() {
|
||||
viewModel.update(homeserver: authenticationService.homeserver.value)
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
private func showForgotPasswordScreen() {
|
||||
viewModel.displayError(.alert("Not implemented."))
|
||||
}
|
||||
}
|
||||
|
@ -7,23 +7,16 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum LoginScreenViewModelAction: CustomStringConvertible {
|
||||
/// Parse the username and update the homeserver if included.
|
||||
case parseUsername(String)
|
||||
/// The user would like to reset their password.
|
||||
case forgotPassword
|
||||
/// Login using the supplied credentials.
|
||||
case login(username: String, password: String)
|
||||
enum LoginScreenViewModelAction {
|
||||
/// The homeserver was updated to one that supports OIDC.
|
||||
case configuredForOIDC
|
||||
/// Login was successful.
|
||||
case signedIn(UserSessionProtocol)
|
||||
|
||||
/// A string representation of the action, ignoring any associated values that could leak PII.
|
||||
var description: String {
|
||||
var isConfiguredForOIDC: Bool {
|
||||
switch self {
|
||||
case .parseUsername:
|
||||
return "parseUsername"
|
||||
case .forgotPassword:
|
||||
return "forgotPassword"
|
||||
case .login:
|
||||
return "login"
|
||||
case .configuredForOIDC: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,7 +27,7 @@ struct LoginScreenViewState: BindableState {
|
||||
/// Whether a new homeserver is currently being loaded.
|
||||
var isLoading = false
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: LoginScreenBindings
|
||||
var bindings = LoginScreenBindings()
|
||||
|
||||
/// The types of login supported by the homeserver.
|
||||
var loginMode: LoginMode { homeserver.loginMode }
|
||||
@ -62,8 +55,6 @@ struct LoginScreenBindings {
|
||||
enum LoginScreenViewAction {
|
||||
/// Parse the username to detect if a homeserver is included.
|
||||
case parseUsername
|
||||
/// The user would like to reset their password.
|
||||
case forgotPassword
|
||||
/// Continue using the input username and password.
|
||||
case next
|
||||
}
|
||||
@ -71,8 +62,10 @@ enum LoginScreenViewAction {
|
||||
enum LoginScreenErrorType: Hashable {
|
||||
/// A specific error message shown in an alert.
|
||||
case alert(String)
|
||||
/// Looking up the homeserver from the username failed.
|
||||
case invalidHomeserver
|
||||
/// An alert that informs the user to check their username/password.
|
||||
case credentialsAlert
|
||||
/// An alert that informs the user that their account has been deactivated.
|
||||
case deactivatedAlert
|
||||
/// An alert that informs the user about a bad well-known file.
|
||||
case invalidWellKnownAlert(String)
|
||||
/// An alert that allows the user to learn about sliding sync.
|
||||
|
@ -11,57 +11,129 @@ import SwiftUI
|
||||
typealias LoginScreenViewModelType = StateStoreViewModel<LoginScreenViewState, LoginScreenViewAction>
|
||||
|
||||
class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol {
|
||||
private let authenticationService: AuthenticationServiceProtocol
|
||||
private let slidingSyncLearnMoreURL: URL
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
|
||||
private var actionsSubject: PassthroughSubject<LoginScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<LoginScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(homeserver: LoginHomeserver, slidingSyncLearnMoreURL: URL) {
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
slidingSyncLearnMoreURL: URL,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
analytics: AnalyticsService) {
|
||||
self.authenticationService = authenticationService
|
||||
self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
|
||||
let bindings = LoginScreenBindings()
|
||||
let viewState = LoginScreenViewState(homeserver: homeserver, bindings: bindings)
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.analytics = analytics
|
||||
|
||||
let viewState = LoginScreenViewState(homeserver: authenticationService.homeserver.value)
|
||||
|
||||
super.init(initialViewState: viewState)
|
||||
|
||||
authenticationService.homeserver
|
||||
.receive(on: DispatchQueue.main)
|
||||
.weakAssign(to: \.state.homeserver, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func process(viewAction: LoginScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .parseUsername:
|
||||
actionsSubject.send(.parseUsername(state.bindings.username))
|
||||
case .forgotPassword:
|
||||
actionsSubject.send(.forgotPassword)
|
||||
parseUsername()
|
||||
case .next:
|
||||
actionsSubject.send(.login(username: state.bindings.username, password: state.bindings.password))
|
||||
login()
|
||||
}
|
||||
}
|
||||
|
||||
func update(isLoading: Bool) {
|
||||
guard state.isLoading != isLoading else { return }
|
||||
state.isLoading = isLoading
|
||||
func stopLoading() {
|
||||
state.isLoading = false
|
||||
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
func update(homeserver: LoginHomeserver) {
|
||||
state.homeserver = homeserver
|
||||
// MARK: - Private
|
||||
|
||||
/// Parses the specified username and looks up the homeserver when a Matrix ID is entered.
|
||||
private func parseUsername() {
|
||||
let username = state.bindings.username
|
||||
|
||||
guard MatrixEntityRegex.isMatrixUserIdentifier(username) else { return }
|
||||
|
||||
let homeserverDomain = String(username.split(separator: ":")[1])
|
||||
|
||||
startLoading(isInteractionBlocking: false)
|
||||
|
||||
Task {
|
||||
switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
|
||||
case .success:
|
||||
if authenticationService.homeserver.value.loginMode == .oidc {
|
||||
actionsSubject.send(.configuredForOIDC)
|
||||
}
|
||||
stopLoading()
|
||||
case .failure(let error):
|
||||
stopLoading()
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func displayError(_ type: LoginScreenErrorType) {
|
||||
switch type {
|
||||
case .alert(let message):
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
/// Requests the authentication coordinator to log in using the specified credentials.
|
||||
private func login() {
|
||||
MXLog.info("Starting login with password.")
|
||||
startLoading(isInteractionBlocking: true)
|
||||
|
||||
Task {
|
||||
analytics.signpost.beginLogin()
|
||||
switch await authenticationService.login(username: state.bindings.username,
|
||||
password: state.bindings.password,
|
||||
initialDeviceName: UIDevice.current.initialDeviceName,
|
||||
deviceID: nil) {
|
||||
case .success(let userSession):
|
||||
actionsSubject.send(.signedIn(userSession))
|
||||
analytics.signpost.endLogin()
|
||||
stopLoading()
|
||||
case .failure(let error):
|
||||
stopLoading()
|
||||
analytics.signpost.endLogin()
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(LoginScreenCoordinatorAction.self)-Loading"
|
||||
|
||||
private func startLoading(isInteractionBlocking: Bool) {
|
||||
if isInteractionBlocking {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
} else {
|
||||
state.isLoading = true
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
MXLog.info("Error occurred: \(error)")
|
||||
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
state.bindings.alertInfo = AlertInfo(id: .credentialsAlert,
|
||||
title: L10n.commonError,
|
||||
message: message)
|
||||
case .invalidHomeserver:
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
message: L10n.screenLoginErrorInvalidCredentials)
|
||||
case .accountDeactivated:
|
||||
state.bindings.alertInfo = AlertInfo(id: .deactivatedAlert,
|
||||
title: L10n.commonError,
|
||||
message: L10n.screenLoginErrorInvalidUserId)
|
||||
case .invalidWellKnownAlert(let error):
|
||||
message: L10n.screenLoginErrorDeactivatedAccount)
|
||||
case .invalidWellKnown(let error):
|
||||
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
|
||||
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,
|
||||
@ -71,12 +143,12 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
||||
|
||||
// Clear out the invalid username to avoid an attempted login to matrix.org
|
||||
state.bindings.username = ""
|
||||
case .refreshTokenAlert:
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
case .sessionTokenRefreshNotSupported:
|
||||
state.bindings.alertInfo = AlertInfo(id: .refreshTokenAlert,
|
||||
title: L10n.commonServerNotSupported,
|
||||
message: L10n.screenLoginErrorRefreshTokens)
|
||||
case .unknown:
|
||||
state.bindings.alertInfo = AlertInfo(id: type)
|
||||
default:
|
||||
state.bindings.alertInfo = AlertInfo(id: .unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,15 +12,6 @@ protocol LoginScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<LoginScreenViewModelAction, Never> { get }
|
||||
var context: LoginScreenViewModelType.Context { get }
|
||||
|
||||
/// Update the view to reflect that a new homeserver is being loaded.
|
||||
/// - Parameter isLoading: Whether or not the homeserver is being loaded.
|
||||
func update(isLoading: Bool)
|
||||
|
||||
/// Update the view with new homeserver information.
|
||||
/// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`.
|
||||
func update(homeserver: LoginHomeserver)
|
||||
|
||||
/// Display an error to the user.
|
||||
/// - Parameter type: The type of error to be displayed.
|
||||
func displayError(_ type: LoginScreenErrorType)
|
||||
/// Update the view to reflect that loaded has finished.
|
||||
func stopLoading()
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ struct LoginScreen: View {
|
||||
// This should never be shown.
|
||||
ProgressView()
|
||||
default:
|
||||
// This should never be shown either.
|
||||
loginUnavailableText
|
||||
}
|
||||
}
|
||||
@ -37,6 +38,7 @@ struct LoginScreen: View {
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert(item: $context.alertInfo)
|
||||
}
|
||||
|
||||
@ -124,35 +126,45 @@ struct LoginScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct LoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let credentialsViewModel: LoginScreenViewModel = {
|
||||
let viewModel = LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
|
||||
viewModel.context.username = "alice"
|
||||
viewModel.context.password = "password"
|
||||
return viewModel
|
||||
}()
|
||||
static let viewModel = makeViewModel()
|
||||
static let credentialsViewModel = makeViewModel(withCredentials: true)
|
||||
static let unconfiguredViewModel = makeViewModel(homeserverAddress: "somethingtofailconfiguration")
|
||||
|
||||
static var previews: some View {
|
||||
screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL))
|
||||
.previewDisplayName("matrix.org")
|
||||
screen(for: credentialsViewModel)
|
||||
.previewDisplayName("Credentials Entered")
|
||||
screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL))
|
||||
.previewDisplayName("Unsupported")
|
||||
screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL))
|
||||
.previewDisplayName("OIDC Fallback")
|
||||
}
|
||||
|
||||
static func screen(for viewModel: LoginScreenViewModel) -> some View {
|
||||
NavigationStack {
|
||||
LoginScreen(context: viewModel.context)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button { } label: {
|
||||
Text("\(Image(systemName: "chevron.backward")) Back")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.previewDisplayName("matrix.org")
|
||||
.snapshotPreferences(delay: 0.1)
|
||||
|
||||
NavigationStack {
|
||||
LoginScreen(context: credentialsViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Credentials Entered")
|
||||
.snapshotPreferences(delay: 0.1)
|
||||
|
||||
NavigationStack {
|
||||
LoginScreen(context: unconfiguredViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Unsupported")
|
||||
.snapshotPreferences(delay: 0.1)
|
||||
}
|
||||
|
||||
static func makeViewModel(homeserverAddress: String = "matrix.org", withCredentials: Bool = false) -> LoginScreenViewModel {
|
||||
let authenticationService = AuthenticationService.mock
|
||||
|
||||
Task { await authenticationService.configure(for: homeserverAddress, flow: .login) }
|
||||
|
||||
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
if withCredentials {
|
||||
viewModel.context.username = "alice"
|
||||
viewModel.context.password = "password"
|
||||
}
|
||||
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
@ -37,10 +37,8 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
||||
|
||||
authenticationService.homeserver
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] homeserver in
|
||||
guard let self else { return }
|
||||
state.homeserverAddress = homeserver.address
|
||||
}
|
||||
.map(\.address)
|
||||
.weakAssign(to: \.state.homeserverAddress, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
@ -204,9 +204,11 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
// MARK: - Mocks
|
||||
|
||||
extension AuthenticationService {
|
||||
static var mock = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
static var mock: AuthenticationService {
|
||||
AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
}
|
||||
}
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Credentials-Entered.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Credentials-Entered.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Credentials-Entered.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Credentials-Entered.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Credentials-Entered.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Credentials-Entered.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
173
UnitTests/Sources/LoginScreenViewModelTests.swift
Normal file
173
UnitTests/Sources/LoginScreenViewModelTests.swift
Normal file
@ -0,0 +1,173 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class LoginScreenViewModelTests: XCTestCase {
|
||||
var viewModel: LoginScreenViewModelProtocol!
|
||||
var context: LoginScreenViewModelType.Context { viewModel.context }
|
||||
|
||||
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
||||
var service: AuthenticationServiceProtocol!
|
||||
|
||||
private func setupViewModel(homeserverAddress: String = "matrix.org") async {
|
||||
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init())
|
||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
clientBuilderFactory: clientBuilderFactory,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
|
||||
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else {
|
||||
XCTFail("A valid server should be configured for the test.")
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = LoginScreenViewModel(authenticationService: service,
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
|
||||
func testMatrixDotOrg() async {
|
||||
// Given the initial view model configured for matrix.org.
|
||||
await setupViewModel()
|
||||
|
||||
// Then the view state should contain a homeserver that matches matrix.org and show the login form.
|
||||
XCTAssertEqual(context.viewState.homeserver, .mockMatrixDotOrg, "The homeserver data should match the default homeserver.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
|
||||
}
|
||||
|
||||
func testBasicServer() async {
|
||||
// Given the view model configured for a basic server example.com that only supports password authentication.
|
||||
await setupViewModel(homeserverAddress: "example.com")
|
||||
|
||||
// Then the view state should be updated with the homeserver and show the login form.
|
||||
XCTAssertEqual(context.viewState.homeserver, .mockBasicServer, "The homeserver data should should match the new homeserver.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
|
||||
}
|
||||
|
||||
func testUsernameWithEmptyPassword() async {
|
||||
// Given a form with an empty username and password.
|
||||
await setupViewModel()
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
|
||||
// When entering a username without a password.
|
||||
context.username = "bob"
|
||||
context.password = ""
|
||||
|
||||
// Then the credentials should be considered invalid.
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
}
|
||||
|
||||
func testEmptyUsernameWithPassword() async {
|
||||
// Given a form with an empty username and password.
|
||||
await setupViewModel()
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
|
||||
// When entering a password without a username.
|
||||
context.username = ""
|
||||
context.password = "12345678"
|
||||
|
||||
// Then the credentials should be considered invalid.
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
}
|
||||
|
||||
func testValidCredentials() async {
|
||||
// Given a form with an empty username and password.
|
||||
await setupViewModel()
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
|
||||
// When entering a username and an 8-character password.
|
||||
context.username = "bob"
|
||||
context.password = "12345678"
|
||||
|
||||
// Then the credentials should be considered valid.
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
}
|
||||
|
||||
func testLoadingServerWithoutPassword() async throws {
|
||||
// Given a form with valid credentials.
|
||||
await setupViewModel()
|
||||
context.username = "@bob:example.com"
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be not be valid without a password.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should not be submittable.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
let deferred = deferFulfillment(context.$viewState, keyPath: \.isLoading, transitionValues: [true, false])
|
||||
context.send(viewAction: .parseUsername)
|
||||
|
||||
// Then the view state should represent the loading but never allow submitting to occur.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should still not be submittable.")
|
||||
}
|
||||
|
||||
func testLoadingServerWithPasswordEntered() async throws {
|
||||
// Given a form with valid credentials.
|
||||
await setupViewModel()
|
||||
context.username = "@bob:example.com"
|
||||
context.password = "12345678"
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
let deferred = deferFulfillment(context.$viewState, keyPath: \.canSubmit, transitionValues: [false, true])
|
||||
context.send(viewAction: .parseUsername)
|
||||
|
||||
// Then the view should be blocked from submitting while loading and then become unblocked again.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
}
|
||||
|
||||
func testOIDCServer() async throws {
|
||||
// Given the screen configured for matrix.org
|
||||
await setupViewModel()
|
||||
|
||||
// When entering a username for a user on a homeserver with OIDC.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isConfiguredForOIDC }
|
||||
context.username = "@bob:company.com"
|
||||
context.send(viewAction: .parseUsername)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the view state should be updated with the homeserver and show the OIDC button.
|
||||
XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.")
|
||||
}
|
||||
|
||||
func testUnsupportedServer() async throws {
|
||||
// Given the screen configured for matrix.org
|
||||
await setupViewModel()
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert when the screen loads.")
|
||||
|
||||
// When entering a username for an unsupported homeserver.
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
context.username = "@bob:server.net"
|
||||
context.send(viewAction: .parseUsername)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the view state should be updated to show an alert.
|
||||
XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.")
|
||||
}
|
||||
}
|
@ -1,137 +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 XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class LoginViewModelTests: XCTestCase {
|
||||
let defaultHomeserver = LoginHomeserver.mockMatrixDotOrg
|
||||
var viewModel: LoginScreenViewModelProtocol!
|
||||
var context: LoginScreenViewModelType.Context!
|
||||
|
||||
@MainActor override func setUp() async throws {
|
||||
viewModel = LoginScreenViewModel(homeserver: defaultHomeserver, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testMatrixDotOrg() {
|
||||
// Given the initial view model configured for matrix.org.
|
||||
let homeserver = defaultHomeserver
|
||||
|
||||
// Then the view state should contain a homeserver that matches matrix.org and show the login form.
|
||||
XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should match the original.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
|
||||
}
|
||||
|
||||
func testBasicServer() {
|
||||
// Given a basic server example.com that only supports password registration.
|
||||
let homeserver = LoginHomeserver.mockBasicServer
|
||||
|
||||
// When updating the view model with the server.
|
||||
viewModel.update(homeserver: homeserver)
|
||||
|
||||
// Then the view state should be updated with the homeserver and show the login form.
|
||||
XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should should match the new homeserver.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
|
||||
}
|
||||
|
||||
func testUsernameWithEmptyPassword() {
|
||||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
|
||||
// When entering a username without a password.
|
||||
context.username = "bob"
|
||||
context.password = ""
|
||||
|
||||
// Then the credentials should be considered invalid.
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
}
|
||||
|
||||
func testEmptyUsernameWithPassword() {
|
||||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
|
||||
// When entering a password without a username.
|
||||
context.username = ""
|
||||
context.password = "12345678"
|
||||
|
||||
// Then the credentials should be considered invalid.
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
}
|
||||
|
||||
func testValidCredentials() {
|
||||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
|
||||
// When entering a username and an 8-character password.
|
||||
context.username = "bob"
|
||||
context.password = "12345678"
|
||||
|
||||
// Then the credentials should be considered valid.
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
}
|
||||
|
||||
func testLoadingServer() {
|
||||
// Given a form with valid credentials.
|
||||
context.username = "bob"
|
||||
context.password = "12345678"
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
viewModel.update(isLoading: true)
|
||||
|
||||
// Then the view state should reflect that the homeserver is loading.
|
||||
XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
|
||||
// When updating the view model after loading a homeserver.
|
||||
viewModel.update(isLoading: false)
|
||||
|
||||
// Then the view state should reflect that the homeserver is now loaded.
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
}
|
||||
|
||||
func testOIDCServer() {
|
||||
// Given a basic server example.com that supports OIDC registration.
|
||||
let homeserver = LoginHomeserver.mockOIDC
|
||||
|
||||
// When updating the view model with the server.
|
||||
viewModel.update(homeserver: homeserver)
|
||||
|
||||
// Then the view state should be updated with the homeserver and show the OIDC button.
|
||||
XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.")
|
||||
}
|
||||
|
||||
func testLogsForPassword() {
|
||||
// Given the coordinator and view model results that contain passwords.
|
||||
let password = "supersecretpassword"
|
||||
let viewModelAction: LoginScreenViewModelAction = .login(username: "Alice", password: password)
|
||||
|
||||
// When creating a string representation of those results (e.g. for logging).
|
||||
let viewModelActionString = "\(viewModelAction)"
|
||||
|
||||
// Then the password should not be included in that string.
|
||||
XCTAssertFalse("\(viewModelActionString)".contains(password), "The password must not be included in any strings.")
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import XCTest
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class ServerSelectionViewModelTests: XCTestCase {
|
||||
class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
||||
var service: AuthenticationServiceProtocol!
|
||||
|
Loading…
x
Reference in New Issue
Block a user