From 701a8a389e479bd153ff89781d3750493338036d Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 14 Jun 2022 17:36:28 +0100 Subject: [PATCH] #40: Create UserSessionStore and update state machine for session restoration. --- ElementX.xcodeproj/project.pbxproj | 34 ++- ElementX/Sources/AppCoordinator.swift | 80 ++++--- .../Sources/AppCoordinatorStateMachine.swift | 31 ++- .../AuthenticationCoordinator.swift | 129 ++++++++++ .../AuthenticationCoordinator.swift | 223 ------------------ .../KeychainController.swift | 0 .../KeychainControllerProtocol.swift | 0 .../UserSession/UserSessionStore.swift | 134 +++++++++++ 8 files changed, 344 insertions(+), 287 deletions(-) create mode 100644 ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift delete mode 100644 ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift rename ElementX/Sources/Services/{Authentication => UserSession}/KeychainController.swift (100%) rename ElementX/Sources/Services/{Authentication => UserSession}/KeychainControllerProtocol.swift (100%) create mode 100644 ElementX/Sources/Services/UserSession/UserSessionStore.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 5eb5e38b2..3f43aaaa5 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; 91D3084B285898A80013EF53 /* OnboardingSplashScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D3084A285898A80013EF53 /* OnboardingSplashScreenUITests.swift */; }; + 91D3084E28589D940013EF53 /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D3084D28589D940013EF53 /* UserSessionStore.swift */; }; 93BA4A81B6D893271101F9F0 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; @@ -442,6 +443,7 @@ 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; 91D3084A285898A80013EF53 /* OnboardingSplashScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSplashScreenUITests.swift; sourceTree = ""; }; + 91D3084D28589D940013EF53 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = ""; }; 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = ""; }; 938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = ""; }; 93B21E72926FACB13A186689 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ml; path = ml.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -652,13 +654,13 @@ 0787F81684E503024BD0C051 /* Services */ = { isa = PBXGroup; children = ( - AAFDD509929A0CCF8BCE51EB /* Authentication */, 0ED3F5C21537519389C07644 /* BugReport */, 8039515BAA53B7C3275AC64A /* Client */, 79E560F5113ED25D172E550C /* Media */, 40E6246F03D1FE377BC5D963 /* Room */, 82D5AD3EAE3A5C1068A44A88 /* Session */, FCDF06BDB123505F0334B4F9 /* Timeline */, + 91D3084C28589D820013EF53 /* UserSession */, ); path = Services; sourceTree = ""; @@ -726,13 +728,6 @@ path = Resources; sourceTree = ""; }; - 298F75357B344DE964106404 /* Login */ = { - isa = PBXGroup; - children = ( - ); - path = Login; - sourceTree = ""; - }; 304D3532D4FFC1F0ABC0626E /* ViewFrameReader */ = { isa = PBXGroup; children = ( @@ -1084,6 +1079,16 @@ path = HTMLParsing; sourceTree = ""; }; + 91D3084C28589D820013EF53 /* UserSession */ = { + isa = PBXGroup; + children = ( + 91D3084D28589D940013EF53 /* UserSessionStore.swift */, + F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */, + 956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */, + ); + path = UserSession; + sourceTree = ""; + }; 9413F680ECDFB2B0DDB0DEF2 /* Packages */ = { isa = PBXGroup; children = ( @@ -1173,16 +1178,6 @@ path = UnitTests; sourceTree = ""; }; - AAFDD509929A0CCF8BCE51EB /* Authentication */ = { - isa = PBXGroup; - children = ( - 0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */, - F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */, - 956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */, - ); - path = Authentication; - sourceTree = ""; - }; AD5FCF9340D670C526AD17E4 /* UI */ = { isa = PBXGroup; children = ( @@ -1309,7 +1304,7 @@ E74CD7681375AD2EAA34D66B /* Authentication */ = { isa = PBXGroup; children = ( - 298F75357B344DE964106404 /* Login */, + 0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */, ); path = Authentication; sourceTree = ""; @@ -1842,6 +1837,7 @@ F01DB7DD607015557CD48B33 /* ViewFrameReader.swift in Sources */, 01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */, 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */, + 91D3084E28589D940013EF53 /* UserSessionStore.swift in Sources */, 50391038BC50C8ED9A4D88A0 /* WeakDictionaryReference.swift in Sources */, 7DE5EB4CB2401C672257283C /* WeakKeyDictionary.swift in Sources */, ); diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 0851e1ab7..384a03862 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -7,7 +7,6 @@ // import UIKit -import Kingfisher class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private let window: UIWindow @@ -19,8 +18,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private let navigationRouter: NavigationRouter - private let keychainController: KeychainControllerProtocol - private let authenticationCoordinator: AuthenticationCoordinator! + private let userSessionStore: UserSessionStore private var userSession: UserSession! @@ -61,14 +59,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { fatalError("Should have a valid bundle identifier at this point") } - keychainController = KeychainController(identifier: bundleIdentifier) - authenticationCoordinator = AuthenticationCoordinator(keychainController: keychainController, - navigationRouter: navigationRouter) + userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier) screenshotDetector = ScreenshotDetector() screenshotDetector.callback = processScreenshotDetection - - authenticationCoordinator.delegate = self setupStateMachine() @@ -84,7 +78,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } func start() { - stateMachine.processEvent(.start) + self.window.makeKeyAndVisible() + stateMachine.processEvent(userSessionStore.hasSessions ? .startWithExistingSession : .startWithAuthentication) } // MARK: - AuthenticationCoordinatorDelegate @@ -93,16 +88,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { stateMachine.processEvent(.attemptedSignIn) } - func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) { - stateMachine.processEvent(.failedSigningIn) - } - - func authenticationCoordinatorDidSetupClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) { + func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSession) { + self.userSession = userSession + remove(childCoordinator: authenticationCoordinator) stateMachine.processEvent(.succeededSigningIn) } - func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) { - stateMachine.processEvent(.succeededSigningOut) + func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) { + stateMachine.processEvent(.failedSigningIn) } // MARK: - Private @@ -111,27 +104,36 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private func setupStateMachine() { stateMachine.addTransitionHandler { [weak self] context in guard let self = self else { return } - + switch (context.fromState, context.event, context.toState) { - case (.initial, .start, .signedOut): - self.window.makeKeyAndVisible() - self.authenticationCoordinator.start() + case (.initial, .startWithAuthentication, .signedOut): + self.showAuthentication() case (.signedOut, .attemptedSignIn, .signingIn): self.showLoadingIndicator() case (.signingIn, .failedSigningIn, .signedOut): self.hideLoadingIndicator() self.showLoginErrorToast() - case (.signingIn, .succeededSigningIn, .signedIn): + case (.signingIn, .succeededSigningIn, .homeScreen): self.hideLoadingIndicator() - self.setupUserSession() - case (.signedIn, .showHomeScreen, .homeScreen): self.presentHomeScreen() + + case (.initial, .startWithExistingSession, .restoringSession): + self.showLoadingIndicator() + self.restoreUserSession() + case (.restoringSession, .failedRestoringSession, .signedOut): + self.hideLoadingIndicator() + self.showLoginErrorToast() + case (.restoringSession, .succeededRestoringSession, .homeScreen): + self.hideLoadingIndicator() + self.presentHomeScreen() + case(_, _, .roomScreen(let roomId)): self.presentRoomWithIdentifier(roomId) case(.roomScreen, .dismissedRoomScreen, .homeScreen): self.tearDownDismissedRoomScreen() case (_, .attemptSignOut, .signingOut): - self.authenticationCoordinator.logout() + self.userSessionStore.logout(userSession: self.userSession) + self.stateMachine.processEvent(.succeededSigningOut) case (.signingOut, .succeededSigningOut, .signedOut): self.tearDownUserSession() case (.signingOut, .failedSigningOut, _): @@ -151,15 +153,26 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } // swiftlint:enable cyclomatic_complexity - private func setupUserSession() { - guard let clientProxy = authenticationCoordinator.clientProxy else { - fatalError("User session should be setup at this point") + private func restoreUserSession() { + Task { + switch await userSessionStore.restoreUserSession() { + case .success(let userSession): + self.userSession = userSession + stateMachine.processEvent(.succeededRestoringSession) + case .failure: + MXLog.error("Failed to restore an existing session.") + stateMachine.processEvent(.failedRestoringSession) + } } + } + + private func showAuthentication() { + let coordinator = AuthenticationCoordinator(userSessionStore: userSessionStore, + navigationRouter: navigationRouter) + coordinator.delegate = self - userSession = .init(clientProxy: clientProxy, - mediaProvider: MediaProvider(clientProxy: clientProxy, imageCache: ImageCache.default)) - - stateMachine.processEvent(.showHomeScreen) + add(childCoordinator: coordinator) + coordinator.start() } private func tearDownUserSession() { @@ -170,7 +183,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { userSession = nil mainNavigationController.setViewControllers([splashViewController], animated: false) - authenticationCoordinator.start() + + showAuthentication() } private func presentHomeScreen() { @@ -259,7 +273,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } private func showLoadingIndicator() { - loadingIndicator = indicatorPresenter.present(.loading(label: "Loading", isInteractionBlocking: true)) + loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) } private func hideLoadingIndicator() { diff --git a/ElementX/Sources/AppCoordinatorStateMachine.swift b/ElementX/Sources/AppCoordinatorStateMachine.swift index 1add9b16c..71ecb668f 100644 --- a/ElementX/Sources/AppCoordinatorStateMachine.swift +++ b/ElementX/Sources/AppCoordinatorStateMachine.swift @@ -18,8 +18,8 @@ class AppCoordinatorStateMachine { case signedOut /// Processing sign in request case signingIn - /// Successfully signed in - case signedIn + /// Opening an existing session. + case restoringSession /// Showing the home screen case homeScreen /// Showing the settings screen @@ -33,22 +33,29 @@ class AppCoordinatorStateMachine { /// Events that can be triggered on the AppCoordinator state machine enum Event: EventType { - /// Start AppCoordinator flows, move from initial - case start + /// Start the `AppCoordinator` by showing authentication. + case startWithAuthentication /// A sign in request has been started case attemptedSignIn - /// Signing it succeeded + /// Signing in succeeded case succeededSigningIn /// Signing in failed case failedSigningIn - /// Request home screen presentation - case showHomeScreen + + /// Start the `AppCoordinator` by restoring an existing account. + case startWithExistingSession + /// Restoring session succeeded. + case succeededRestoringSession + /// Restoring session failed. + case failedRestoringSession + /// Request sign out case attemptSignOut /// Signing out succeeded case succeededSigningOut /// Signing out failed case failedSigningOut + /// Request presentation for a particular room /// - Parameter roomId:the room identifier case showRoomScreen(roomId: String) @@ -64,14 +71,14 @@ class AppCoordinatorStateMachine { init() { stateMachine = StateMachine(state: .initial) { machine in - machine.addRoutes(event: .start, transitions: [ .initial => .signedOut ]) - + machine.addRoutes(event: .startWithAuthentication, transitions: [ .initial => .signedOut ]) machine.addRoutes(event: .attemptedSignIn, transitions: [ .signedOut => .signingIn ]) - - machine.addRoutes(event: .succeededSigningIn, transitions: [ .signingIn => .signedIn ]) + machine.addRoutes(event: .succeededSigningIn, transitions: [ .signingIn => .homeScreen ]) machine.addRoutes(event: .failedSigningIn, transitions: [ .signingIn => .signedOut ]) - machine.addRoutes(event: .showHomeScreen, transitions: [ .signedIn => .homeScreen ]) + machine.addRoutes(event: .startWithExistingSession, transitions: [ .initial => .restoringSession ]) + machine.addRoutes(event: .succeededRestoringSession, transitions: [ .restoringSession => .homeScreen ]) + machine.addRoutes(event: .failedRestoringSession, transitions: [ .restoringSession => .signedOut ]) machine.addRoutes(event: .attemptSignOut, transitions: [ .homeScreen => .signingOut ]) diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift new file mode 100644 index 000000000..86fd09664 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -0,0 +1,129 @@ +// +// AuthenticationCoordinator.swift +// ElementX +// +// Created by Stefan Ceriu on 11.02.2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import MatrixRustSDK + +enum AuthenticationCoordinatorError: Error { + case failedLoggingIn +} + +@MainActor +protocol AuthenticationCoordinatorDelegate: AnyObject { + + func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) + + func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, + didLoginWithSession userSession: UserSession) + + func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, + didFailWithError error: AuthenticationCoordinatorError) +} + +class AuthenticationCoordinator: Coordinator { + + private let userSessionStore: UserSessionStore + private let navigationRouter: NavigationRouter + + private(set) var clientProxy: ClientProxyProtocol? + var childCoordinators: [Coordinator] = [] + + weak var delegate: AuthenticationCoordinatorDelegate? + + init(userSessionStore: UserSessionStore, + navigationRouter: NavigationRouter) { + self.userSessionStore = userSessionStore + self.navigationRouter = navigationRouter + } + + func start() { + showSplashScreen() + } + + // MARK: - Private + + private func showSplashScreen() { + let coordinator = SplashScreenCoordinator() + + coordinator.callback = { [weak self] action in + guard let self = self else { return } + switch action { + case .login: + self.startNewLoginFlow() + case .register: + fatalError("Not implemented") + } + } + + add(childCoordinator: coordinator) + navigationRouter.setRootModule(coordinator) + + coordinator.start() + } + + private func startNewLoginFlow() { + let parameters = LoginScreenCoordinatorParameters() + let coordinator = LoginScreenCoordinator(parameters: parameters) + + coordinator.callback = { [weak self, weak coordinator] action in + guard let self = self, let coordinator = coordinator else { + return + } + + switch action { + case .login(let result): + Task { + switch await self.login(username: result.username, password: result.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)") + } + } + } + } + + add(childCoordinator: coordinator) + navigationRouter.push(coordinator) + + coordinator.start() + } + + private func login(username: String, password: String) async -> Result { + Benchmark.startTrackingForIdentifier("Login", message: "Started new login") + + delegate?.authenticationCoordinatorDidStartLoading(self) + + let basePath = userSessionStore.baseDirectoryPath(for: username) + let loginTask = Task.detached { + try loginNewClient(basePath: basePath, + username: username, + password: password) + } + + 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 { + switch await userSessionStore.userSession(for: client) { + case .success(let clientProxy): + return .success(clientProxy) + case .failure: + return .failure(.failedLoggingIn) + } + } +} diff --git a/ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift deleted file mode 100644 index 6f9dfd160..000000000 --- a/ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift +++ /dev/null @@ -1,223 +0,0 @@ -// -// AuthenticationCoordinator.swift -// ElementX -// -// Created by Stefan Ceriu on 11.02.2022. -// Copyright © 2022 Element. All rights reserved. -// - -import Foundation -import MatrixRustSDK - -enum AuthenticationCoordinatorError: Error { - case failedLoggingIn - case failedRestoringLogin - case failedSettingUpSession -} - -@MainActor -protocol AuthenticationCoordinatorDelegate: AnyObject { - - func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) - - func authenticationCoordinatorDidSetupClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) - - func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) - - func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, - didFailWithError error: AuthenticationCoordinatorError) -} - -class AuthenticationCoordinator: Coordinator { - - private let keychainController: KeychainControllerProtocol - private let navigationRouter: NavigationRouter - - private(set) var clientProxy: ClientProxyProtocol? - var childCoordinators: [Coordinator] = [] - - weak var delegate: AuthenticationCoordinatorDelegate? - - init(keychainController: KeychainControllerProtocol, - navigationRouter: NavigationRouter) { - self.keychainController = keychainController - self.navigationRouter = navigationRouter - } - - func start() { - - let availableAccessTokens = keychainController.accessTokens() - - guard let usernameTokenTuple = availableAccessTokens.first else { - showSplashScreen() - return - } - - Task { - switch await restorePreviousLogin(usernameTokenTuple) { - case .success: - self.delegate?.authenticationCoordinatorDidSetupClientProxy(self) - case .failure(let error): - self.delegate?.authenticationCoordinator(self, didFailWithError: error) - MXLog.error("Failed restoring login with error: \(error)") - - // On any restoration failure reset the token and restart - self.keychainController.removeAllAccessTokens() - self.start() - } - } - } - - func logout() { - keychainController.removeAllAccessTokens() - - if let userIdentifier = clientProxy?.userIdentifier { - deleteBaseDirectoryForUsername(userIdentifier) - } - - clientProxy = nil - - delegate?.authenticationCoordinatorDidTearDownClientProxy(self) - } - - // MARK: - Private - - private func showSplashScreen() { - let coordinator = SplashScreenCoordinator() - - coordinator.callback = { [weak self] action in - guard let self = self else { return } - switch action { - case .login: - self.startNewLoginFlow { result in - switch result { - case .success: - self.delegate?.authenticationCoordinatorDidSetupClientProxy(self) - case .failure(let error): - self.delegate?.authenticationCoordinator(self, didFailWithError: error) - MXLog.error("Failed logging in user with error: \(error)") - } - } - case .register: - fatalError("Not implemented") - } - } - - add(childCoordinator: coordinator) - navigationRouter.setRootModule(coordinator) - - coordinator.start() - } - - private func startNewLoginFlow(_ completion: @escaping (Result<(), AuthenticationCoordinatorError>) -> Void) { - let parameters = LoginScreenCoordinatorParameters() - let coordinator = LoginScreenCoordinator(parameters: parameters) - - coordinator.callback = { [weak self, weak coordinator] action in - guard let self = self, let coordinator = coordinator else { - return - } - - switch action { - case .login(let result): - Task { - switch await self.login(username: result.username, password: result.password) { - case .success: - completion(.success(())) - self.remove(childCoordinator: coordinator) - self.navigationRouter.dismissModule() - case .failure(let error): - completion(.failure(error)) - } - - } - } - } - - add(childCoordinator: coordinator) - navigationRouter.setRootModule(coordinator) - - coordinator.start() - } - - private func login(username: String, password: String) async -> Result { - Benchmark.startTrackingForIdentifier("Login", message: "Started new login") - - delegate?.authenticationCoordinatorDidStartLoading(self) - - let basePath = baseDirectoryPathForUsername(username) - let loginTask = Task.detached { - try loginNewClient(basePath: basePath, - username: username, - password: password) - } - - switch await loginTask.result { - case .success(let client): - return await setupProxyForClient(client) - case .failure(let error): - MXLog.error("Failed logging in with error: \(error)") - return .failure(.failedLoggingIn) - } - } - - private func restorePreviousLogin(_ usernameTokenTuple: (username: String, accessToken: String)) async -> Result { - Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login") - - delegate?.authenticationCoordinatorDidStartLoading(self) - - let basePath = baseDirectoryPathForUsername(usernameTokenTuple.username) - let loginTask = Task.detached { - try loginWithToken(basePath: basePath, - restoreToken: usernameTokenTuple.accessToken) - } - - switch await loginTask.result { - case .success(let client): - return await setupProxyForClient(client) - case .failure(let error): - MXLog.error("Failed restoring login with error: \(error)") - return .failure(.failedRestoringLogin) - } - } - - private func setupProxyForClient(_ client: Client) async -> Result { - Benchmark.endTrackingForIdentifier("Login", message: "Finished login") - - do { - let accessToken = try client.restoreToken() - let userId = try client.userId() - - keychainController.setAccessToken(accessToken, forUsername: userId) - } catch { - MXLog.error("Failed setting up user session with error: \(error)") - return .failure(.failedSettingUpSession) - } - - clientProxy = ClientProxy(client: client) - - return .success(()) - } - - private func baseDirectoryPathForUsername(_ username: String) -> String { - guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { - fatalError("Should always be able to retrieve the caches directory") - } - - url = url.appendingPathComponent(username) - - try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) - - return url.path - } - - private func deleteBaseDirectoryForUsername(_ username: String) { - guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { - fatalError("Should always be able to retrieve the caches directory") - } - - url = url.appendingPathComponent(username) - - try? FileManager.default.removeItem(at: url) - } -} diff --git a/ElementX/Sources/Services/Authentication/KeychainController.swift b/ElementX/Sources/Services/UserSession/KeychainController.swift similarity index 100% rename from ElementX/Sources/Services/Authentication/KeychainController.swift rename to ElementX/Sources/Services/UserSession/KeychainController.swift diff --git a/ElementX/Sources/Services/Authentication/KeychainControllerProtocol.swift b/ElementX/Sources/Services/UserSession/KeychainControllerProtocol.swift similarity index 100% rename from ElementX/Sources/Services/Authentication/KeychainControllerProtocol.swift rename to ElementX/Sources/Services/UserSession/KeychainControllerProtocol.swift diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift new file mode 100644 index 000000000..cbefa108f --- /dev/null +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -0,0 +1,134 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixRustSDK +import Kingfisher + +enum UserSessionStoreError: Error { + case missingCredentials + case failedRestoringLogin + case failedSettingUpSession +} + +@MainActor +class UserSessionStore { + + private let keychainController: KeychainControllerProtocol + + /// Whether or not there are sessions in the store. + var hasSessions: Bool { !keychainController.accessTokens().isEmpty } + + init(bundleIdentifier: String) { + keychainController = KeychainController(identifier: bundleIdentifier) + } + + func restoreUserSession() async -> Result { + let availableAccessTokens = keychainController.accessTokens() + + guard let usernameTokenTuple = availableAccessTokens.first else { + return .failure(.missingCredentials) + } + + switch await restorePreviousLogin(usernameTokenTuple) { + case .success(let clientProxy): + return .success(UserSession(clientProxy: clientProxy, + mediaProvider: MediaProvider(clientProxy: clientProxy, imageCache: ImageCache.default))) + case .failure(let error): + MXLog.error("Failed restoring login with error: \(error)") + + // On any restoration failure reset the token and restart + self.keychainController.removeAllAccessTokens() + deleteBaseDirectory(for: usernameTokenTuple.username) + + return .failure(error) + } + } + + func userSession(for client: Client) async -> Result { + switch await setupProxyForClient(client) { + case .success(let clientProxy): + return .success(UserSession(clientProxy: clientProxy, + mediaProvider: MediaProvider(clientProxy: clientProxy, imageCache: ImageCache.default))) + case .failure(let error): + MXLog.error("Failed creating user session with error: \(error)") + return .failure(error) + } + } + + func logout(userSession: UserSessionProtocol) { + keychainController.removeAllAccessTokens() + deleteBaseDirectory(for: userSession.clientProxy.userIdentifier) + } + + private func restorePreviousLogin(_ usernameTokenTuple: (username: String, accessToken: String)) async -> Result { + Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login") + + let basePath = baseDirectoryPath(for: usernameTokenTuple.username) + let loginTask = Task.detached { + try loginWithToken(basePath: basePath, + restoreToken: usernameTokenTuple.accessToken) + } + + switch await loginTask.result { + case .success(let client): + return await setupProxyForClient(client) + case .failure(let error): + MXLog.error("Failed restoring login with error: \(error)") + return .failure(.failedRestoringLogin) + } + } + + private func setupProxyForClient(_ client: Client) async -> Result { + Benchmark.endTrackingForIdentifier("Login", message: "Finished login") + + do { + let accessToken = try client.restoreToken() + let userId = try client.userId() + + keychainController.setAccessToken(accessToken, forUsername: userId) + } catch { + MXLog.error("Failed setting up user session with error: \(error)") + return .failure(.failedSettingUpSession) + } + + let clientProxy = ClientProxy(client: client) + + return .success((clientProxy)) + } + + func baseDirectoryPath(for username: String) -> String { + guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + fatalError("Should always be able to retrieve the caches directory") + } + + url = url.appendingPathComponent(username) + + try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) + + return url.path + } + + private func deleteBaseDirectory(for username: String) { + guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + fatalError("Should always be able to retrieve the caches directory") + } + + url = url.appendingPathComponent(username) + + try? FileManager.default.removeItem(at: url) + } +}