#40: Add UserSessionStoreProtocol.

Only log out of the specific account.
Add tests for the keychain controller.
Expand test coverage.
PR comments
This commit is contained in:
Doug
2022-06-15 10:10:22 +01:00
committed by Doug
parent abd6a9c7c0
commit 8653f1dd6b
17 changed files with 261 additions and 78 deletions
+8 -8
View File
@@ -18,9 +18,9 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private let navigationRouter: NavigationRouter
private let userSessionStore: UserSessionStore
private let userSessionStore: UserSessionStoreProtocol
private var userSession: UserSession!
private var userSession: UserSessionProtocol!
private let memberDetailProviderManager: MemberDetailProviderManager
@@ -88,7 +88,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
stateMachine.processEvent(.attemptedSignIn)
}
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSession) {
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession
remove(childCoordinator: authenticationCoordinator)
stateMachine.processEvent(.succeededSigningIn)
@@ -100,14 +100,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
// MARK: - Private
// swiftlint:disable cyclomatic_complexity
// swiftlint:disable cyclomatic_complexity function_body_length
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.showAuthentication()
self.startAuthentication()
case (.signedOut, .attemptedSignIn, .signingIn):
self.showLoadingIndicator()
case (.signingIn, .failedSigningIn, .signedOut):
@@ -151,7 +151,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
fatalError("Failed transition with context: \(context)")
}
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable cyclomatic_complexity function_body_length
private func restoreUserSession() {
Task {
@@ -166,7 +166,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
}
}
private func showAuthentication() {
private func startAuthentication() {
let coordinator = AuthenticationCoordinator(userSessionStore: userSessionStore,
navigationRouter: navigationRouter)
coordinator.delegate = self
@@ -184,7 +184,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
mainNavigationController.setViewControllers([splashViewController], animated: false)
showAuthentication()
startAuthentication()
}
private func presentHomeScreen() {
@@ -19,7 +19,7 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator)
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
didLoginWithSession userSession: UserSession)
didLoginWithSession userSession: UserSessionProtocol)
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
didFailWithError error: AuthenticationCoordinatorError)
@@ -27,7 +27,7 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
class AuthenticationCoordinator: Coordinator {
private let userSessionStore: UserSessionStore
private let userSessionStore: UserSessionStoreProtocol
private let navigationRouter: NavigationRouter
private(set) var clientProxy: ClientProxyProtocol?
@@ -35,7 +35,7 @@ class AuthenticationCoordinator: Coordinator {
weak var delegate: AuthenticationCoordinatorDelegate?
init(userSessionStore: UserSessionStore,
init(userSessionStore: UserSessionStoreProtocol,
navigationRouter: NavigationRouter) {
self.userSessionStore = userSessionStore
self.navigationRouter = navigationRouter
@@ -16,13 +16,11 @@
import SwiftUI
/// Metrics used across the entire onboarding flow.
struct AuthenticationMetrics {
/// Standard constants used across the app's UI.
struct UIConstants {
static let maxContentWidth: CGFloat = 600
static let maxContentHeight: CGFloat = 750
/// The padding used between the top of the main content and the navigation bar.
static let topPaddingToNavigationBar: CGFloat = 16
/// The width/height used for the main icon shown in most of the screens.
static let iconSize: CGFloat = 90
}
@@ -16,11 +16,7 @@
import SwiftUI
protocol SplashScreenCoordinatorProtocol: Coordinator, Presentable {
var callback: ((SplashScreenCoordinatorAction) -> Void)? { get set }
}
final class SplashScreenCoordinator: SplashScreenCoordinatorProtocol {
final class SplashScreenCoordinator: Coordinator, Presentable {
// MARK: - Properties
@@ -40,7 +40,7 @@ enum SplashScreenViewModelAction {
// MARK: View
struct SplashScreenViewState: BindableState, CustomDebugStringConvertible {
struct SplashScreenViewState: BindableState {
private enum Constants {
static let gradientColors = [
Color(red: 0.95, green: 0.98, blue: 0.96),
@@ -55,11 +55,6 @@ struct SplashScreenViewState: BindableState, CustomDebugStringConvertible {
let content: [SplashScreenPageContent]
var bindings: SplashScreenBindings
/// Custom debug description to reduce noise in the logs.
var debugDescription: String {
"SplashScreenViewState at page \(bindings.pageIndex)."
}
init() {
// The pun doesn't translate, so we only use it for English.
let locale = Locale.current
@@ -51,6 +51,7 @@ struct SplashScreen: View {
overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom)
.frame(width: geometry.size.width)
.tag(-1)
.accessibilityIdentifier("hiddenPage")
ForEach(0..<pageCount, id: \.self) { index in
SplashScreenPage(content: viewModel.viewState.content[index],
@@ -90,7 +91,7 @@ struct SplashScreen: View {
buttons
.padding(.horizontal, 16)
.frame(maxWidth: AuthenticationMetrics.maxContentWidth)
.frame(maxWidth: UIConstants.maxContentWidth)
Spacer()
}
.background(ViewFrameReader(frame: $overlayFrame))
@@ -68,8 +68,8 @@ struct SplashScreenPage: View {
Spacer().frame(maxHeight: overlayHeight)
}
.padding(.horizontal, 16)
.frame(maxWidth: AuthenticationMetrics.maxContentWidth,
maxHeight: AuthenticationMetrics.maxContentHeight)
.frame(maxWidth: UIConstants.maxContentWidth,
maxHeight: UIConstants.maxContentHeight)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(backgroundGradient.ignoresSafeArea())
@@ -20,7 +20,7 @@ class KeychainController: KeychainControllerProtocol {
do {
try keychain.set(accessToken, key: username)
} catch {
MXLog.error("Failed storing user restore token with error: \(error)")
MXLog.error("Failed storing user access token with error: \(error)")
}
}
@@ -28,7 +28,7 @@ class KeychainController: KeychainControllerProtocol {
do {
return try keychain.get(username)
} catch {
MXLog.error("Failed retrieving user restore token")
MXLog.error("Failed retrieving user access token")
return nil
}
}
@@ -43,6 +43,14 @@ class KeychainController: KeychainControllerProtocol {
}
}
func removeAccessTokenForUsername(_ username: String) {
do {
try keychain.remove(username)
} catch {
MXLog.error("Failed removing access token with error: \(error)")
}
}
func removeAllAccessTokens() {
do {
try keychain.removeAll()
@@ -12,5 +12,6 @@ protocol KeychainControllerProtocol {
func setAccessToken(_ accessToken: String, forUsername username: String)
func accessTokenForUsername(_ username: String) -> String?
func accessTokens() -> [(username: String, accessToken: String)]
func removeAccessTokenForUsername(_ username: String)
func removeAllAccessTokens()
}
@@ -18,14 +18,7 @@ import Foundation
import MatrixRustSDK
import Kingfisher
enum UserSessionStoreError: Error {
case missingCredentials
case failedRestoringLogin
case failedSettingUpSession
}
@MainActor
class UserSessionStore {
class UserSessionStore: UserSessionStoreProtocol {
private let keychainController: KeychainControllerProtocol
@@ -70,8 +63,9 @@ class UserSessionStore {
}
func logout(userSession: UserSessionProtocol) {
keychainController.removeAllAccessTokens()
deleteBaseDirectory(for: userSession.clientProxy.userIdentifier)
let username = userSession.clientProxy.userIdentifier
keychainController.removeAccessTokenForUsername(username)
deleteBaseDirectory(for: username)
}
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, accessToken: String)) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
@@ -0,0 +1,42 @@
//
// 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
enum UserSessionStoreError: Error {
case missingCredentials
case failedRestoringLogin
case failedSettingUpSession
}
@MainActor
protocol UserSessionStoreProtocol {
/// Whether or not there are sessions in the store.
var hasSessions: Bool { get }
/// Restores an existing user session.
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError>
/// Creates a user session for a new client from the SDK.
func userSession(for client: Client) async -> Result<UserSession, UserSessionStoreError>
/// Logs out of the specified session.
func logout(userSession: UserSessionProtocol)
/// Returns the location to store user data for a particular username.
func baseDirectoryPath(for username: String) -> String
}
+3 -1
View File
@@ -22,6 +22,8 @@ class UITestsAppCoordinator: Coordinator {
window.tintColor = .element.accent
let screens = mockScreens()
screens.forEach { $0.coordinator.start() }
let rootView = UITestsRootView(mockScreens: screens) { id in
guard let screen = screens.first(where: { $0.id == id }) else {
fatalError()
@@ -52,5 +54,5 @@ class UITestsAppCoordinator: Coordinator {
struct MockScreen: Identifiable {
let id: String
let coordinator: Presentable
let coordinator: Coordinator & Presentable
}