Create sessions through the user session manager

Sign-in now creates and stores its session through the
UserSessionManagerProtocol rather than the UserSessionStore, so the
AppCoordinator no longer holds the store.

- The manager forwards clientSessionDelegate and
  userSession(for:sessionDirectories:passphrase:) to its store. As with
  restoring, the new session isn't registered until the AppCoordinator
  adds it.
- AuthenticationService takes the manager, for both sign-in and soft
  logout.
- The store mock's configuration moves to the manager mock.
This commit is contained in:
Mauro Romito
2026-10-08 21:09:09 +02:00
committed by Mauro
parent 60db364436
commit 5792a8ba5c
11 changed files with 91 additions and 30 deletions
@@ -18,7 +18,6 @@ import Version
class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDelegate, NotificationManagerDelegate, SecureWindowManagerDelegate {
private let stateMachine: AppCoordinatorStateMachine
private let navigationRootCoordinator: NavigationRootCoordinator
private let userSessionStore: UserSessionStoreProtocol
private let userSessionManager: UserSessionManagerProtocol
// periphery:ignore - retaining purpose
private let targetConfiguration: Target.ConfigurationResult
@@ -117,11 +116,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
let keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
userSessionStore = UserSessionStore(keychainController: keychainController,
appSettings: appSettings,
analyticsService: analyticsService,
appHooks: appHooks,
networkMonitor: networkMonitor)
let userSessionStore = UserSessionStore(keychainController: keychainController,
appSettings: appSettings,
analyticsService: analyticsService,
appHooks: appHooks,
networkMonitor: networkMonitor)
userSessionManager = UserSessionManager(userSessionStore: userSessionStore, appSettings: appSettings)
let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
@@ -681,7 +680,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private func startAuthentication() {
let encryptionKeyProvider = EncryptionKeyProvider()
let classicAppManager = ClassicAppManager()
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
let authenticationService = AuthenticationService(userSessionManager: userSessionManager,
encryptionKeyProvider: encryptionKeyProvider,
classicAppManager: classicAppManager,
appSettings: appSettings,
@@ -736,7 +735,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
userDisplayName: userSession.clientProxy.userProfilePublisher.value.displayName ?? "",
deviceID: userSession.clientProxy.deviceID)
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
let authenticationService = AuthenticationService(userSessionManager: userSessionManager,
encryptionKeyProvider: EncryptionKeyProvider(),
classicAppManager: ClassicAppManager(),
appSettings: appSettings,
@@ -14818,6 +14818,11 @@ nonisolated class UserSessionManagerMock: UserSessionManagerProtocol, @unchecked
}
nonisolated(unsafe) var underlyingSessionsPublisher: CurrentValuePublisher<[UserSessionProtocol], Never>!
nonisolated(unsafe) var activeSession: UserSessionProtocol?
var clientSessionDelegate: ClientSessionDelegate {
get { return underlyingClientSessionDelegate }
set(value) { underlyingClientSessionDelegate = value }
}
nonisolated(unsafe) var underlyingClientSessionDelegate: ClientSessionDelegate!
//MARK: - session
@@ -14889,6 +14894,48 @@ nonisolated class UserSessionManagerMock: UserSessionManagerProtocol, @unchecked
return restoreActiveSessionReturnValue
}
}
//MARK: - userSession
private let userSessionForSessionDirectoriesPassphraseCallsCountLock = NSLock()
private nonisolated(unsafe) var userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = 0
var userSessionForSessionDirectoriesPassphraseCallsCount: Int {
get { userSessionForSessionDirectoriesPassphraseCallsCountLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount } }
set { userSessionForSessionDirectoriesPassphraseCallsCountLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue } }
}
var userSessionForSessionDirectoriesPassphraseCalled: Bool {
return userSessionForSessionDirectoriesPassphraseCallsCount > 0
}
private let userSessionForSessionDirectoriesPassphraseReceivedArgumentsLock = NSLock()
private nonisolated(unsafe) var userSessionForSessionDirectoriesPassphraseUnderlyingReceivedArguments: (client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data)?
var userSessionForSessionDirectoriesPassphraseReceivedArguments: (client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data)? {
get { userSessionForSessionDirectoriesPassphraseReceivedArgumentsLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingReceivedArguments } }
set { userSessionForSessionDirectoriesPassphraseReceivedArgumentsLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingReceivedArguments = newValue } }
}
private let userSessionForSessionDirectoriesPassphraseReceivedInvocationsLock = NSLock()
private nonisolated(unsafe) var userSessionForSessionDirectoriesPassphraseUnderlyingReceivedInvocations: [(client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data)] = []
var userSessionForSessionDirectoriesPassphraseReceivedInvocations: [(client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data)] {
get { userSessionForSessionDirectoriesPassphraseReceivedInvocationsLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingReceivedInvocations } }
set { userSessionForSessionDirectoriesPassphraseReceivedInvocationsLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingReceivedInvocations = newValue } }
}
private let userSessionForSessionDirectoriesPassphraseReturnValueLock = NSLock()
private nonisolated(unsafe) var userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionStoreError>!
var userSessionForSessionDirectoriesPassphraseReturnValue: Result<UserSessionProtocol, UserSessionStoreError>! {
get { userSessionForSessionDirectoriesPassphraseReturnValueLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue } }
set { userSessionForSessionDirectoriesPassphraseReturnValueLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue } }
}
nonisolated(unsafe) var userSessionForSessionDirectoriesPassphraseClosure: ((ClientProtocol, SessionDirectories, Data) async -> Result<UserSessionProtocol, UserSessionStoreError>)?
@concurrent func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data) async -> Result<UserSessionProtocol, UserSessionStoreError> {
userSessionForSessionDirectoriesPassphraseCallsCountLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount += 1 }
userSessionForSessionDirectoriesPassphraseReceivedArguments = (client: client, sessionDirectories: sessionDirectories, passphrase: passphrase)
userSessionForSessionDirectoriesPassphraseReceivedInvocationsLock.withLock { userSessionForSessionDirectoriesPassphraseUnderlyingReceivedInvocations.append((client: client, sessionDirectories: sessionDirectories, passphrase: passphrase)) }
if let userSessionForSessionDirectoriesPassphraseClosure = userSessionForSessionDirectoriesPassphraseClosure {
return await userSessionForSessionDirectoriesPassphraseClosure(client, sessionDirectories, passphrase)
} else {
return userSessionForSessionDirectoriesPassphraseReturnValue
}
}
//MARK: - add
private let addCallsCountLock = NSLock()
@@ -1,12 +1,11 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2024-2025 New Vector Ltd.
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
extension UserSessionStoreMock {
extension UserSessionManagerMock {
struct Configuration { }
convenience init(_ configuration: Configuration) {
@@ -15,7 +15,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
private var sessionDirectories: SessionDirectories
private let passphrase: Data
private let userSessionStore: UserSessionStoreProtocol
private let userSessionManager: UserSessionManagerProtocol
private let classicAppManager: ClassicAppManagerProtocol?
private let clientFactory: ClientFactoryProtocol
private let appSettings: AppSettings
@@ -33,7 +33,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
let classicAppAccount: ClassicAppAccount?
init(userSessionStore: UserSessionStoreProtocol,
init(userSessionManager: UserSessionManagerProtocol,
encryptionKeyProvider: EncryptionKeyProviderProtocol,
classicAppManager: ClassicAppManagerProtocol?,
clientFactory: ClientFactoryProtocol = ClientFactory(),
@@ -42,7 +42,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
sessionDirectories = .init()
passphrase = encryptionKeyProvider.generateKey()
self.userSessionStore = userSessionStore
self.userSessionManager = userSessionManager
self.classicAppManager = classicAppManager
self.clientFactory = clientFactory
self.appSettings = appSettings
@@ -270,7 +270,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
let client = try await clientFactory.makeAuthenticationClient(serverNameOrBaseURL: serverNameOrBaseURL,
sessionDirectories: sessionDirectories,
passphrase: passphrase,
clientSessionDelegate: userSessionStore.clientSessionDelegate,
clientSessionDelegate: userSessionManager.clientSessionDelegate,
appSettings: appSettings,
appHooks: appHooks)
try await appHooks.remoteSettingsHook.initializeCache(using: client, applyingTo: appSettings).get()
@@ -284,7 +284,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
}
private func userSession(for client: ClientProtocol) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
switch await userSessionManager.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
case .success(let clientProxy):
return .success(clientProxy)
case .failure:
@@ -302,7 +302,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
do {
let client = try await clientFactory.makeInMemoryClient(serverNameOrBaseURL: classicAppAccount.homeserverURL.absoluteString,
clientSessionDelegate: userSessionStore.clientSessionDelegate,
clientSessionDelegate: userSessionManager.clientSessionDelegate,
appSettings: appSettings,
appHooks: appHooks)
let loginDetails = await client.homeserverLoginDetails()
@@ -406,7 +406,7 @@ extension AuthenticationService {
}
static func mock(classicAppManager: ClassicAppManagerProtocol?) -> AuthenticationService {
AuthenticationService(userSessionStore: UserSessionStoreMock(.init()),
AuthenticationService(userSessionManager: UserSessionManagerMock(.init()),
encryptionKeyProvider: EncryptionKeyProvider(),
classicAppManager: classicAppManager,
clientFactory: ClientFactoryMock(.init()),
@@ -7,6 +7,7 @@
import Combine
import Foundation
import MatrixRustSDK
import OrderedCollections
/// Owns the live session of every account signed in on this device, most recently active first.
@@ -37,6 +38,10 @@ final class UserSessionManager: UserSessionManagerProtocol {
accounts.values.first ?? nil
}
var clientSessionDelegate: ClientSessionDelegate {
userSessionStore.clientSessionDelegate
}
init(userSessionStore: UserSessionStoreProtocol, appSettings: AppSettings) {
self.userSessionStore = userSessionStore
self.appSettings = appSettings
@@ -79,6 +84,10 @@ final class UserSessionManager: UserSessionManagerProtocol {
return .failure(.failedRestoringSessions)
}
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data) async -> Result<UserSessionProtocol, UserSessionStoreError> {
await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase)
}
func add(_ userSession: UserSessionProtocol) {
let userID = userSession.clientProxy.userID
@@ -7,6 +7,7 @@
import Combine
import Foundation
import MatrixRustSDK
enum UserSessionManagerError: Error {
/// There are no signed in accounts to restore.
@@ -23,6 +24,8 @@ protocol UserSessionManagerProtocol: AnyObject {
var sessionsPublisher: CurrentValuePublisher<[UserSessionProtocol], Never> { get }
/// The session of the account whose UI is on screen.
var activeSession: UserSessionProtocol? { get }
/// The delegate that stores changes to a client's session, such as refreshed tokens.
var clientSessionDelegate: ClientSessionDelegate { get }
func session(for userID: String) -> UserSessionProtocol?
@@ -32,6 +35,10 @@ protocol UserSessionManagerProtocol: AnyObject {
///
/// The session isn't registered: `add` it once it's ready to be used, e.g. after any migrations.
func restoreActiveSession() async -> Result<UserSessionProtocol, UserSessionManagerError>
/// Creates the session of an account that has just signed in, and stores its credentials.
///
/// The session isn't registered: `add` it once it's ready to be used.
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data) async -> Result<UserSessionProtocol, UserSessionStoreError>
/// Registers a session that's ready to be used, replacing any previous session for its account.
/// A new account becomes the active one, a known account keeps its place.
func add(_ userSession: UserSessionProtocol)