Show recovery instead of verification if this is the last session and recovery is set up

* Show recovery instead of verification if this is the last session and recovery is set up

* Rename `recoveryKeyState` to `recoveryState`

* Remove duplicates on session security state changes.

* Fix missing "Save recovery key" button

* Fix unit tests

* Rename `isLastDevice` to `isOnlyDeviceLeft`

* Address PR comments
This commit is contained in:
Stefan Ceriu
2024-02-16 11:38:49 +02:00
committed by GitHub
parent e0c9f43026
commit db05540cc5
23 changed files with 255 additions and 208 deletions
@@ -25,5 +25,5 @@ struct MockUserSession: UserSessionProtocol {
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
var sessionVerificationState: CurrentValuePublisher<Bool?, Never> = .init(.init(true))
var sessionSecurityStatePublisher: AnyPublisher<SessionSecurityState, Never> = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .enabled)).eraseToAnyPublisher()
}
@@ -18,10 +18,12 @@ import Combine
import Foundation
class UserSession: UserSessionProtocol {
private let sessionVerificationStateSubject: CurrentValueSubject<SessionVerificationState, Never> = .init(.unknown)
private var cancellables = Set<AnyCancellable>()
private var checkSessionVerificationStateCancellable: AnyCancellable?
private var checkSessionVerificationStateTask: Task<Void, Never>?
private var retrieveSessionVerificationControllerTask: Task<Void, Never>?
private var authErrorCancellable: AnyCancellable?
@@ -40,7 +42,7 @@ class UserSession: UserSessionProtocol {
sessionVerificationController?.callbacks.sink { [weak self] callback in
switch callback {
case .finished:
self?.sessionVerificationStateSubject.send(true)
self?.sessionVerificationStateSubject.send(.verified)
default:
break
}
@@ -49,74 +51,31 @@ class UserSession: UserSessionProtocol {
}
}
private var sessionVerificationStateSubject: CurrentValueSubject<Bool?, Never> = .init(nil)
var sessionVerificationState: CurrentValuePublisher<Bool?, Never> {
sessionVerificationStateSubject.asCurrentValuePublisher()
}
let sessionSecurityStatePublisher: AnyPublisher<SessionSecurityState, Never>
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol) {
self.clientProxy = clientProxy
self.mediaProvider = mediaProvider
self.voiceMessageMediaManager = voiceMessageMediaManager
setupSessionVerificationWatchdog()
setupAuthErrorWatchdog()
}
// MARK: - Private
private func setupSessionVerificationWatchdog() {
checkSessionVerificationStateCancellable = clientProxy.callbacks
sessionSecurityStatePublisher = Publishers.CombineLatest(sessionVerificationStateSubject, clientProxy.secureBackupController.recoveryState)
.map {
MXLog.info("Session security state changed, verificationState: \($0), recoveryState: \($1)")
return SessionSecurityState(verificationState: $0, recoveryState: $1)
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
clientProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
if callback.isSyncUpdate {
self?.attemptSessionVerification()
self?.checkSessionVerificationState()
}
}
}
private func attemptSessionVerification() {
guard checkSessionVerificationStateTask == nil else {
MXLog.info("Session verification state check already in progress")
return
}
.store(in: &cancellables)
MXLog.info("Checking session verification state")
checkSessionVerificationStateTask = Task {
MXLog.info("Retrieving session verification controller")
switch await clientProxy.sessionVerificationControllerProxy() {
case .success(let sessionVerificationController):
MXLog.info("Retrieving session verification state")
guard case let .success(isVerified) = await sessionVerificationController.isVerified() else {
MXLog.error("Failed checking verification state. Will retry on the next sync update.")
return
}
tearDownSessionVerificationControllerWatchdog()
self.sessionVerificationController = sessionVerificationController
MXLog.info("Session verified: \(isVerified)")
sessionVerificationStateSubject.send(isVerified)
checkSessionVerificationStateTask = nil
case .failure(let error):
MXLog.info("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
}
}
}
private func tearDownSessionVerificationControllerWatchdog() {
checkSessionVerificationStateCancellable = nil
}
// MARK: Auth Error Watchdog
private func setupAuthErrorWatchdog() {
authErrorCancellable = clientProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
@@ -124,14 +83,68 @@ class UserSession: UserSessionProtocol {
switch callback {
case .receivedAuthError(let isSoftLogout):
callbacks.send(.didReceiveAuthError(isSoftLogout: isSoftLogout))
tearDownAuthErrorWatchdog()
authErrorCancellable = nil
default:
break
}
}
}
private func tearDownAuthErrorWatchdog() {
authErrorCancellable = nil
// MARK: - Private
private func checkSessionVerificationState() {
guard retrieveSessionVerificationControllerTask == nil else {
MXLog.info("Session verification state check already in progress")
return
}
guard sessionVerificationController == nil else {
Task {
await updateSessionVerificationState()
}
return
}
MXLog.info("Retrieving session verification controller")
retrieveSessionVerificationControllerTask = Task {
switch await clientProxy.sessionVerificationControllerProxy() {
case .success(let sessionVerificationController):
self.sessionVerificationController = sessionVerificationController
await updateSessionVerificationState()
retrieveSessionVerificationControllerTask = nil
case .failure(let error):
MXLog.info("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
}
}
}
private func updateSessionVerificationState() async {
guard let sessionVerificationController else {
fatalError("This point should never be reached")
}
MXLog.info("Checking session verification state")
guard case let .success(isVerified) = await sessionVerificationController.isVerified() else {
MXLog.error("Failed checking verification state. Will retry on the next sync update.")
return
}
if isVerified {
sessionVerificationStateSubject.send(.verified)
} else {
guard case let .success(isLastDevice) = await clientProxy.isOnlyDeviceLeft() else {
MXLog.error("Failed checking isLastDevice. Will retry on the next sync update.")
return
}
if isLastDevice {
sessionVerificationStateSubject.send(.unverifiedLastSession)
} else {
sessionVerificationStateSubject.send(.unverified)
}
}
}
}
@@ -21,6 +21,18 @@ enum UserSessionCallback {
case didReceiveAuthError(isSoftLogout: Bool)
}
enum SessionVerificationState {
case unknown
case verified
case unverified
case unverifiedLastSession
}
struct SessionSecurityState: Equatable {
let verificationState: SessionVerificationState
let recoveryState: SecureBackupRecoveryState
}
protocol UserSessionProtocol {
var homeserver: String { get }
var userID: String { get }
@@ -30,7 +42,7 @@ protocol UserSessionProtocol {
var mediaProvider: MediaProviderProtocol { get }
var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get }
var sessionVerificationState: CurrentValuePublisher<Bool?, Never> { get }
var sessionSecurityStatePublisher: AnyPublisher<SessionSecurityState, Never> { get }
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
var callbacks: PassthroughSubject<UserSessionCallback, Never> { get }