Introduce a user session manager (#6295)

This commit is contained in:
Mauro
2026-10-09 14:07:39 +02:00
committed by GitHub
parent 65bab49f39
commit 60db364436
26 changed files with 594 additions and 66 deletions
@@ -19,6 +19,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
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
private let appMediator: AppMediator
@@ -38,18 +39,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private var userSessionMigrationsOldVersion: Version?
private var userSession: UserSessionProtocol? {
didSet {
userSessionObserver?.cancel()
if let userSession {
configureElementCallService()
configureNotificationManager()
observeUserSessionChanges()
Task {
await resumeClientServices()
await appHooks.configure(with: userSession)
}
}
}
userSessionManager.activeSession
}
private var authenticationFlowCoordinator: AuthenticationFlowCoordinator?
@@ -132,6 +122,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
analyticsService: analyticsService,
appHooks: appHooks,
networkMonitor: networkMonitor)
userSessionManager = UserSessionManager(userSessionStore: userSessionStore, appSettings: appSettings)
let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
let appLockNavigationCoordinator = NavigationRootCoordinator()
@@ -216,7 +207,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
return
}
guard userSessionStore.hasSessions else {
guard !userSessionManager.userIDs.isEmpty else {
stateMachine.processEvent(.startWithAuthentication)
return
}
@@ -379,7 +370,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
// MARK: - AuthenticationFlowCoordinatorDelegate
func authenticationFlowCoordinator(didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession
userSessionManager.add(userSession)
configureActiveSession()
authenticationFlowCoordinator = nil
stateMachine.processEvent(.createdUserSession)
}
@@ -580,7 +572,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
appSettings.resetAllSettings()
appLockFlowCoordinator.appLockService.disable()
}
userSessionStore.reset()
userSessionManager.reset()
}
/// Manually cleans up any files in the app group's `tmp` directory.
@@ -673,10 +665,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private func restoreUserSession() {
Task {
switch await userSessionStore.restoreUserSession() {
switch await userSessionManager.restoreActiveSession() {
case .success(let userSession):
await self.performUserSessionMigrations(userSession)
self.userSession = userSession
userSessionManager.add(userSession)
configureActiveSession()
stateMachine.processEvent(.createdUserSession)
case .failure:
MXLog.error("Failed to restore an existing session.")
@@ -765,7 +758,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
switch action {
case .signedIn(let session):
self.userSession = session
self.userSessionManager.add(session)
self.configureActiveSession()
self.softLogoutCoordinator = nil
stateMachine.processEvent(.createdUserSession)
case .clearAllData:
@@ -793,6 +787,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
let flowParameters = CommonFlowParameters(userSession: userSession,
availableSessionsPublisher: userSessionManager.sessionsPublisher.map { $0.map(UserSessionDetails.init) },
bugReportService: bugReportService,
elementCallService: elementCallService,
timelineControllerFactory: TimelineControllerFactory(userSettings: userSession.userSettings),
@@ -867,7 +862,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
await userSession.clientProxy.logout()
// Regardless of the result, clear user data
userSessionStore.logout(userSession: userSession)
userSessionManager.remove(userID: userSession.clientProxy.userID)
tearDownUserSession()
userSession.userSettings.account.reset()
@@ -886,7 +881,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private func tearDownUserSession() {
userIndicatorController.retractAllIndicators()
userSession = nil
userSessionObserver?.cancel()
userSessionFlowCoordinator = nil
@@ -912,15 +907,28 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
}
private func configureElementCallService() {
/// Points the app-wide services at the active account's session.
private func configureActiveSession() {
guard let userSession else {
fatalError("User session not setup")
}
elementCallService.setUserSession(userSession)
configureServices(for: userSession)
Task {
await resumeClientServices()
await appHooks.configure(with: userSession)
}
}
private func configureNotificationManager() {
/// Wires the services that work for every signed in account, not only the active one. They only hold a single session for now.
private func configureServices(for userSession: UserSessionProtocol) {
userSessionObserver?.cancel()
elementCallService.setUserSession(userSession)
configureNotificationManager(for: userSession)
observeUserSessionChanges(userSession)
}
private func configureNotificationManager(for userSession: UserSessionProtocol) {
notificationManager.setUserSession(userSession)
appDelegateObserver = appDelegate.callbacks
@@ -935,11 +943,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
}
private func observeUserSessionChanges() {
guard let userSession else {
fatalError("User session not setup")
}
private func observeUserSessionChanges(_ userSession: UserSessionProtocol) {
userSessionObserver = userSession.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
@@ -1354,7 +1358,7 @@ private extension AppCoordinator {
/// When iOS relaunches a terminated app for a background task, the session needs restoring before the task can run.
private func waitForSessionRestore() async {
// A background launch doesn't necessarily connect a scene, which is what normally calls `start()`.
if stateMachine.state == .initial, userSessionStore.hasSessions {
if stateMachine.state == .initial, !userSessionManager.userIDs.isEmpty {
start()
}
@@ -24,7 +24,10 @@ extension FlowCoordinatorProtocol {
///
/// Please do **not** pass this type directly to screen coordinators/view models.
struct CommonFlowParameters {
/// The session of the account this flow tree belongs to. It never changes, even when another account becomes active.
let userSession: UserSessionProtocol
/// Every signed in account, most recently active first, so the first one is `userSession`'s.
let availableSessionsPublisher: CurrentValuePublisher<[UserSessionDetails], Never>
let bugReportService: BugReportServiceProtocol
let elementCallService: ElementCallServiceProtocol
let timelineControllerFactory: TimelineControllerFactoryProtocol
@@ -14,6 +14,12 @@ final nonisolated class AccountSettings: @unchecked Sendable {
private let store: AccountUserDefaults
private let userID: String
// MARK: Accounts
/// When this account was last selected, used to order the accounts most recently selected first.
@UserPreference
var lastSelectedDate: Date?
// MARK: Session
@UserPreference(defaultValue: false)