Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic AuthenticationService and missing UI tests. #126

Merged
merged 6 commits into from
Jul 4, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

21 changes: 4 additions & 17 deletions ElementX/Sources/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,37 +89,23 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {

// MARK: - AuthenticationCoordinatorDelegate

func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) {
stateMachine.processEvent(.attemptedSignIn)
}

func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession
remove(childCoordinator: authenticationCoordinator)
stateMachine.processEvent(.succeededSigningIn)
}

func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) {
stateMachine.processEvent(.failedSigningIn)
}

// MARK: - Private

// swiftlint:disable cyclomatic_complexity function_body_length
// swiftlint:disable cyclomatic_complexity
private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in
guard let self = self else { return }

switch (context.fromState, context.event, context.toState) {
case (.initial, .startWithAuthentication, .signedOut):
self.startAuthentication()
case (.signedOut, .attemptedSignIn, .signingIn):
self.showLoadingIndicator()
case (.signingIn, .failedSigningIn, .signedOut):
self.hideLoadingIndicator()
self.showLoginErrorToast()
case (.signingIn, .succeededSigningIn, .homeScreen):
self.hideLoadingIndicator()
case (.signedOut, .succeededSigningIn, .homeScreen):
self.presentHomeScreen()

case (.initial, .startWithExistingSession, .restoringSession):
Expand Down Expand Up @@ -179,7 +165,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
}

private func startAuthentication() {
let coordinator = AuthenticationCoordinator(userSessionStore: userSessionStore,
let authenticationService = AuthenticationService(userSessionStore: userSessionStore)
let coordinator = AuthenticationCoordinator(authenticationService: authenticationService,
navigationRouter: navigationRouter)
coordinator.delegate = self

Expand Down
10 changes: 1 addition & 9 deletions ElementX/Sources/AppCoordinatorStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ class AppCoordinatorStateMachine {
case initial
/// Showing the login screen
case signedOut
/// Processing sign in request
case signingIn
/// Opening an existing session.
case restoringSession
/// Showing the home screen
Expand All @@ -41,12 +39,8 @@ class AppCoordinatorStateMachine {
enum Event: EventType {
/// Start the `AppCoordinator` by showing authentication.
case startWithAuthentication
/// A sign in request has been started
case attemptedSignIn
/// Signing in succeeded
case succeededSigningIn
/// Signing in failed
case failedSigningIn

/// Start the `AppCoordinator` by restoring an existing account.
case startWithExistingSession
Expand Down Expand Up @@ -84,9 +78,7 @@ class AppCoordinatorStateMachine {
init() {
stateMachine = StateMachine(state: .initial) { machine in
machine.addRoutes(event: .startWithAuthentication, transitions: [ .initial => .signedOut ])
machine.addRoutes(event: .attemptedSignIn, transitions: [ .signedOut => .signingIn ])
machine.addRoutes(event: .succeededSigningIn, transitions: [ .signingIn => .homeScreen ])
machine.addRoutes(event: .failedSigningIn, transitions: [ .signingIn => .signedOut ])
machine.addRoutes(event: .succeededSigningIn, transitions: [ .signedOut => .homeScreen ])

machine.addRoutes(event: .startWithExistingSession, transitions: [ .initial => .restoringSession ])
machine.addRoutes(event: .succeededRestoringSession, transitions: [ .restoringSession => .homeScreen ])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,39 @@
// Copyright © 2022 Element. All rights reserved.
//

import Foundation
import UIKit
import MatrixRustSDK

enum AuthenticationCoordinatorError: Error {
case failedLoggingIn
}

@MainActor
protocol AuthenticationCoordinatorDelegate: AnyObject {

func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator)

func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
didLoginWithSession userSession: UserSessionProtocol)

func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
didFailWithError error: AuthenticationCoordinatorError)
}

class AuthenticationCoordinator: Coordinator {
class AuthenticationCoordinator: Coordinator, Presentable {

private let userSessionStore: UserSessionStoreProtocol
private let authenticationService: AuthenticationServiceProtocol
private let navigationRouter: NavigationRouter

private(set) var clientProxy: ClientProxyProtocol?
var childCoordinators: [Coordinator] = []

weak var delegate: AuthenticationCoordinatorDelegate?

init(userSessionStore: UserSessionStoreProtocol,
init(authenticationService: AuthenticationServiceProtocol,
navigationRouter: NavigationRouter) {
self.userSessionStore = userSessionStore
self.authenticationService = authenticationService
self.navigationRouter = navigationRouter
}

func start() {
showSplashScreen()
}

func toPresentable() -> UIViewController {
navigationRouter.toPresentable()
}

// MARK: - Private

private func showSplashScreen() {
Expand All @@ -67,28 +61,18 @@ class AuthenticationCoordinator: Coordinator {
}

private func showLoginScreen() {
let homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString)
let parameters = LoginCoordinatorParameters(navigationRouter: navigationRouter, homeserver: homeserver)
let parameters = LoginCoordinatorParameters(authenticationService: authenticationService,
navigationRouter: navigationRouter)
let coordinator = LoginCoordinator(parameters: parameters)

coordinator.callback = { [weak self, weak coordinator] action in
guard let self = self, let coordinator = coordinator else {
return
}
guard let self = self, let coordinator = coordinator else { return }

switch action {
case .login(let username, let password):
Task {
switch await self.login(username: username, password: password) {
case .success(let userSession):
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
self.remove(childCoordinator: coordinator)
self.navigationRouter.dismissModule()
case .failure(let error):
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
MXLog.error("Failed logging in user with error: \(error)")
}
}
case .signedIn(let userSession):
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
self.remove(childCoordinator: coordinator)
self.navigationRouter.dismissModule()
case .continueWithOIDC:
break
}
Expand All @@ -101,38 +85,4 @@ class AuthenticationCoordinator: Coordinator {
self?.remove(childCoordinator: coordinator)
}
}

private func login(username: String, password: String) async -> Result<UserSession, AuthenticationCoordinatorError> {
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")

delegate?.authenticationCoordinatorDidStartLoading(self)

let basePath = userSessionStore.baseDirectoryPath(for: username)
let builder = ClientBuilder()
.basePath(path: basePath)
.username(username: username)

let loginTask: Task<Client, Error> = Task.detached {
let client = try builder.build()
try client.login(username: username, password: password)
return client
}

switch await loginTask.result {
case .success(let client):
return await userSession(for: client)
case .failure(let error):
MXLog.error("Failed logging in with error: \(error)")
return .failure(.failedLoggingIn)
}
}

private func userSession(for client: Client) async -> Result<UserSession, AuthenticationCoordinatorError> {
switch await userSessionStore.userSession(for: client) {
case .success(let clientProxy):
return .success(clientProxy)
case .failure:
return .failure(.failedLoggingIn)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,17 @@ import SwiftUI
import MatrixRustSDK

struct LoginCoordinatorParameters {
/// The service used to authenticate the user.
let authenticationService: AuthenticationServiceProtocol
/// The navigation router used to present the server selection screen.
let navigationRouter: NavigationRouterType
/// The homeserver to be shown initially.
let homeserver: LoginHomeserver
}

enum LoginCoordinatorAction: CustomStringConvertible {
/// Login with the associated username and password.
case login(username: String, password: String)
enum LoginCoordinatorAction {
/// Login was successful.
case signedIn(UserSessionProtocol)
/// Continue using OIDC.
case continueWithOIDC

/// A string representation of the action, ignoring any associated values that could leak PII.
var description: String {
switch self {
case .login:
return "login"
case .continueWithOIDC:
return "continueWithOIDC"
}
}
}

final class LoginCoordinator: Coordinator, Presentable {
Expand All @@ -56,6 +47,7 @@ final class LoginCoordinator: Coordinator, Presentable {
}
}

private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService }
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var activityIndicator: UserIndicator?
Expand All @@ -71,7 +63,7 @@ final class LoginCoordinator: Coordinator, Presentable {
init(parameters: LoginCoordinatorParameters) {
self.parameters = parameters

let viewModel = LoginViewModel(homeserver: parameters.homeserver)
let viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver)
loginViewModel = viewModel

let view = LoginScreen(context: viewModel.context)
Expand Down Expand Up @@ -135,56 +127,68 @@ final class LoginCoordinator: Coordinator, Presentable {
}

/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: Error) {
loginViewModel.displayError(.alert(error.localizedDescription))
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidCredentials:
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
case .accountDeactivated:
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
default:
loginViewModel.displayError(.alert(ElementL10n.unknownError))
}
}

/// Requests the authentication coordinator to log in using the specified credentials.
private func login(username: String, password: String) {
var username = loginViewModel.context.username
startLoading(isInteractionBlocking: true)

if !isMXID(username: username) {
let homeserver = loginViewModel.context.viewState.homeserver
username = "@\(username):\(homeserver.address)"
Task {
switch await authenticationService.login(username: username, password: password) {
case .success(let userSession):
callback?(.signedIn(userSession))
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}

callback?(.login(username: username, password: password))
}

/// Parses the specified username and looks up the homeserver when a Matrix ID is entered.
private func parseUsername(_ username: String) {
guard isMXID(username: username) else { return }
guard authenticationService.usernameIsMatrixID(username) else { return }
pixlwave marked this conversation as resolved.
Show resolved Hide resolved

let domain = String(username.split(separator: ":")[1])
let homeserverDomain = String(username.split(separator: ":")[1])

let homeserver = LoginHomeserver(address: domain)
updateViewModel(homeserver: homeserver)
indicateSuccess()
}

/// Checks whether the specified username is a Matrix ID or not.
private func isMXID(username: String) -> Bool {
let range = NSRange(location: 0, length: username.count)
startLoading(isInteractionBlocking: false)

let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive)
return detector?.numberOfMatches(in: username, range: range) ?? 0 > 0
Task {
switch await authenticationService.startLogin(for: homeserverDomain) {
case .success:
updateViewModel()
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
}

/// Updates the view model with a different homeserver.
private func updateViewModel(homeserver: LoginHomeserver) {
loginViewModel.update(homeserver: homeserver)
private func updateViewModel() {
loginViewModel.update(homeserver: authenticationService.homeserver)
indicateSuccess()
}

/// Presents the server selection screen as a modal.
private func presentServerSelectionScreen() {
MXLog.debug("[LoginCoordinator] presentServerSelectionScreen")
let parameters = ServerSelectionCoordinatorParameters(homeserver: loginViewModel.context.viewState.homeserver,
let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
hasModalPresentation: true)
let coordinator = ServerSelectionCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] result in
coordinator.callback = { [weak self, weak coordinator] action in
guard let self = self, let coordinator = coordinator else { return }
self.serverSelectionCoordinator(coordinator, didCompleteWith: result)
self.serverSelectionCoordinator(coordinator, didCompleteWith: action)
}

coordinator.start()
Expand All @@ -198,10 +202,10 @@ final class LoginCoordinator: Coordinator, Presentable {

/// Handles the result from the server selection modal, dismissing it after updating the view.
private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator,
didCompleteWith result: ServerSelectionCoordinatorResult) {
didCompleteWith action: ServerSelectionCoordinatorAction) {
navigationRouter.dismissModule(animated: true) { [weak self] in
if case let .selected(homeserver) = result {
self?.updateViewModel(homeserver: homeserver)
if action == .updated {
self?.updateViewModel()
}

self?.remove(childCoordinator: coordinator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ struct LoginServerInfoSection: View {
.padding(.vertical, 2)
}
.buttonStyle(.elementGhost())
.accessibilityIdentifier("editServerButton")
}
}
}
Expand Down
Loading