#40: Create UserSessionStore and update state machine for session restoration.
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; };
|
||||
91D3084A285898A80013EF53 /* OnboardingSplashScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSplashScreenUITests.swift; sourceTree = "<group>"; };
|
||||
91D3084D28589D940013EF53 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = "<group>"; };
|
||||
92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = "<group>"; };
|
||||
938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
93B21E72926FACB13A186689 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ml; path = ml.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -726,13 +728,6 @@
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
298F75357B344DE964106404 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Login;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
304D3532D4FFC1F0ABC0626E /* ViewFrameReader */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1084,6 +1079,16 @@
|
||||
path = HTMLParsing;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
91D3084C28589D820013EF53 /* UserSession */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
91D3084D28589D940013EF53 /* UserSessionStore.swift */,
|
||||
F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */,
|
||||
956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */,
|
||||
);
|
||||
path = UserSession;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9413F680ECDFB2B0DDB0DEF2 /* Packages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1173,16 +1178,6 @@
|
||||
path = UnitTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AAFDD509929A0CCF8BCE51EB /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */,
|
||||
F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */,
|
||||
956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */,
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AD5FCF9340D670C526AD17E4 /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1309,7 +1304,7 @@
|
||||
E74CD7681375AD2EAA34D66B /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
298F75357B344DE964106404 /* Login */,
|
||||
0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */,
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 ])
|
||||
|
||||
|
||||
@@ -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<UserSession, AuthenticationCoordinatorError> {
|
||||
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<UserSession, AuthenticationCoordinatorError> {
|
||||
switch await userSessionStore.userSession(for: client) {
|
||||
case .success(let clientProxy):
|
||||
return .success(clientProxy)
|
||||
case .failure:
|
||||
return .failure(.failedLoggingIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Void, AuthenticationCoordinatorError> {
|
||||
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<Void, AuthenticationCoordinatorError> {
|
||||
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<Void, AuthenticationCoordinatorError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
134
ElementX/Sources/Services/UserSession/UserSessionStore.swift
Normal file
134
ElementX/Sources/Services/UserSession/UserSessionStore.swift
Normal file
@@ -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<UserSession, UserSessionStoreError> {
|
||||
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<UserSession, UserSessionStoreError> {
|
||||
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<ClientProxyProtocol, UserSessionStoreError> {
|
||||
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<ClientProxyProtocol, UserSessionStoreError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user