Files
letro-ios/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift
Doug cb5c7337d2 Add support for Account Provisioning links. (#4108)
* Add support for account provisioning links and route them to the authentication flow.

* Use the provisioning parameters to configure the authentication flow.

* Add UI tests for the provisioned authentication flow.

* Record new preview snapshots.

* Add unit tests.

* Make the domain configurable in the app settings.

* Use the loginHint in the login screen too.
2025-05-12 13:28:34 +01:00

158 lines
6.7 KiB
Swift

//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
typealias LoginScreenViewModelType = StateStoreViewModel<LoginScreenViewState, LoginScreenViewAction>
class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol {
private let authenticationService: AuthenticationServiceProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let analytics: AnalyticsService
private var actionsSubject: PassthroughSubject<LoginScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<LoginScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(authenticationService: AuthenticationServiceProtocol,
loginHint: String?,
userIndicatorController: UserIndicatorControllerProtocol,
analytics: AnalyticsService) {
self.authenticationService = authenticationService
self.userIndicatorController = userIndicatorController
self.analytics = analytics
let username = switch loginHint {
case .some(let hint) where hint.hasPrefix("mxid:"): String(hint.dropFirst(5)) // MSC4198
case .some(let hint): hint
case .none: ""
}
let viewState = LoginScreenViewState(homeserver: authenticationService.homeserver.value,
bindings: LoginScreenBindings(username: username))
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:
parseUsername()
case .next:
login()
}
}
func stopLoading() {
state.isLoading = false
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
// 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.supportsOIDCFlow {
actionsSubject.send(.configuredForOIDC)
}
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
}
/// 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: L10n.screenLoginErrorInvalidCredentials)
case .accountDeactivated:
state.bindings.alertInfo = AlertInfo(id: .deactivatedAlert,
title: L10n.commonError,
message: L10n.screenLoginErrorDeactivatedAccount)
case .invalidWellKnown(let error):
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorInvalidWellKnown(error))
case .slidingSyncNotAvailable:
let nonBreakingAppName = InfoPlistReader.main.bundleDisplayName.replacingOccurrences(of: " ", with: "\u{00A0}")
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorNoSlidingSyncMessage(nonBreakingAppName))
// Clear out the invalid username to avoid an attempted login to matrix.org
state.bindings.username = ""
case .sessionTokenRefreshNotSupported:
state.bindings.alertInfo = AlertInfo(id: .refreshTokenAlert,
title: L10n.commonServerNotSupported,
message: L10n.screenLoginErrorRefreshTokens)
default:
state.bindings.alertInfo = AlertInfo(id: .unknown)
}
}
}