diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 002fe0759..1997565b3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -567,6 +567,7 @@ "screen_session_verification_compare_numbers_subtitle" = "Confirm that the numbers below match those shown on your other session."; "screen_session_verification_compare_numbers_title" = "Compare numbers"; "screen_session_verification_complete_subtitle" = "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."; +"screen_session_verification_enter_recovery_key" = "Enter recovery key"; "screen_session_verification_open_existing_session_subtitle" = "Prove it’s you in order to access your encrypted message history."; "screen_session_verification_open_existing_session_title" = "Open an existing session"; "screen_session_verification_positive_button_canceled" = "Retry verification"; diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 0878bec67..f47eab276 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -208,7 +208,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { fatalError("The sessionVerificationController should aways be valid at this point") } - let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController) + let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController, + recoveryState: parameters.userSession.sessionSecurityStatePublisher.value.recoveryState) let coordinator = SessionVerificationScreenCoordinator(parameters: verificationParameters) coordinator.actions @@ -216,6 +217,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { guard let self else { return } switch action { + case .recoveryKey: + navigationStackCoordinator.setSheetCoordinator(nil) + handleAppRoute(.chatBackupSettings, animated: true) case .done: navigationStackCoordinator.setSheetCoordinator(nil) } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index e281aa949..d84111f0d 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -270,7 +270,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { break case (.roomList, .showLogoutConfirmationScreen, .logoutConfirmationScreen): - presentSecureBackupConfirmationScreen() + presentSecureBackupLogoutConfirmationScreen() case (.logoutConfirmationScreen, .dismissedLogoutConfirmationScreen, .roomList): break @@ -400,7 +400,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { return } - presentSecureBackupConfirmationScreen() + presentSecureBackupLogoutConfirmationScreen() } // MARK: Session verification @@ -410,7 +410,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { fatalError("The sessionVerificationController should aways be valid at this point") } - let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController) + let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController, + recoveryState: userSession.sessionSecurityStatePublisher.value.recoveryState) let coordinator = SessionVerificationScreenCoordinator(parameters: parameters) @@ -419,6 +420,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { guard let self else { return } switch action { + case .recoveryKey: + settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true) case .done: navigationSplitCoordinator.setSheetCoordinator(nil) } @@ -511,7 +514,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { // MARK: Secure backup confirmation - private func presentSecureBackupConfirmationScreen() { + private func presentSecureBackupLogoutConfirmationScreen() { let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, networkMonitor: ServiceLocator.shared.networkMonitor)) diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 52bb9fd3b..b1a898cf6 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -1392,6 +1392,8 @@ public enum L10n { public static var screenSessionVerificationCompareNumbersTitle: String { return L10n.tr("Localizable", "screen_session_verification_compare_numbers_title") } /// Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted. public static var screenSessionVerificationCompleteSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_complete_subtitle") } + /// Enter recovery key + public static var screenSessionVerificationEnterRecoveryKey: String { return L10n.tr("Localizable", "screen_session_verification_enter_recovery_key") } /// Prove it’s you in order to access your encrypted message history. public static var screenSessionVerificationOpenExistingSessionSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_open_existing_session_subtitle") } /// Open an existing session diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 47a6518e9..80ff5f6e8 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -198,6 +198,7 @@ enum A11yIdentifiers { let emojiWrapper = "session_verification-emojis" let verificationComplete = "session_verification-verification_complete" let close = "session_verification-close" + let enterRecoveryKey = "session_verification-enter_recovery_key" } struct SettingsScreen { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 809fcc8d2..640556d37 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -75,7 +75,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol case (.unverifiedLastSession, .incomplete): state.requiresExtraAccountSetup = true if state.securityBannerMode != .dismissed { - state.securityBannerMode = .recoveryKeyConfirmation + state.securityBannerMode = .sessionVerification } case (.verified, .disabled): state.requiresExtraAccountSetup = true diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift index 6ec439aef..4e664470f 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift @@ -18,11 +18,13 @@ import Combine import SwiftUI enum SessionVerificationScreenCoordinatorAction { + case recoveryKey case done } struct SessionVerificationScreenCoordinatorParameters { let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol + let recoveryState: SecureBackupRecoveryState } final class SessionVerificationScreenCoordinator: CoordinatorProtocol { @@ -36,7 +38,8 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol { } init(parameters: SessionVerificationScreenCoordinatorParameters) { - viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy) + viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy, + recoveryState: parameters.recoveryState) } // MARK: - Public @@ -47,6 +50,8 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { + case .recoveryKey: + actionsSubject.send(.recoveryKey) case .finished: actionsSubject.send(.done) } diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenModels.swift b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenModels.swift index 4b13756f7..b2c5c9da7 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenModels.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenModels.swift @@ -17,10 +17,13 @@ import Foundation enum SessionVerificationScreenViewModelAction { + case recoveryKey case finished } struct SessionVerificationScreenViewState: BindableState { + let showRecoveryOption: Bool + var verificationState: SessionVerificationScreenStateMachine.State = .initial var title: String? { @@ -83,6 +86,7 @@ struct SessionVerificationScreenViewState: BindableState { } enum SessionVerificationScreenViewAction { + case recoveryKey case requestVerification case startSasVerification case restart diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift index 48f3ed89d..47d8b2f18 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift @@ -31,12 +31,13 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess } init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol, - initialState: SessionVerificationScreenViewState = SessionVerificationScreenViewState()) { + recoveryState: SecureBackupRecoveryState, + verificationState: SessionVerificationScreenStateMachine.State = .initial) { self.sessionVerificationControllerProxy = sessionVerificationControllerProxy stateMachine = SessionVerificationScreenStateMachine() - super.init(initialViewState: initialState) + super.init(initialViewState: .init(showRecoveryOption: recoveryState == .incomplete, verificationState: verificationState)) setupStateMachine() @@ -70,6 +71,8 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess override func process(viewAction: SessionVerificationScreenViewAction) { switch viewAction { + case .recoveryKey: + actionsSubject.send(.recoveryKey) case .requestVerification: stateMachine.processEvent(.requestVerification) case .startSasVerification: diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift b/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift index 0d21f5921..cbb1af994 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift @@ -128,12 +128,21 @@ struct SessionVerificationScreen: View { private var actionButtons: some View { switch context.viewState.verificationState { case .initial: - Button(L10n.actionStartVerification) { - context.send(viewAction: .requestVerification) + VStack(spacing: 32) { + Button(L10n.actionStartVerification) { + context.send(viewAction: .requestVerification) + } + .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.requestVerification) + + if context.viewState.showRecoveryOption { + Button(L10n.screenSessionVerificationEnterRecoveryKey) { + context.send(viewAction: .recoveryKey) + } + .buttonStyle(.compound(.plain)) + .accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.enterRecoveryKey) + } } - .buttonStyle(.compound(.primary)) - .accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.requestVerification) - case .cancelled: Button(L10n.actionRetry) { context.send(viewAction: .restart) @@ -235,7 +244,8 @@ struct SessionVerification_Previews: PreviewProvider, TestablePreview { static func sessionVerificationScreen(state: SessionVerificationScreenStateMachine.State) -> some View { let viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: SessionVerificationControllerProxyMock.configureMock(), - initialState: SessionVerificationScreenViewState(verificationState: state)) + recoveryState: .incomplete, + verificationState: state) return SessionVerificationScreen(context: viewModel.context) } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 97bd36d43..0411575c3 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -55,7 +55,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo state.securitySectionMode = .sessionVerification case (.unverifiedLastSession, .incomplete): state.showSecuritySectionBadge = true - state.securitySectionMode = .secureBackup + state.securitySectionMode = .sessionVerification case (.verified, .disabled): state.showSecuritySectionBadge = true state.securitySectionMode = .secureBackup diff --git a/ElementX/Sources/Services/Session/MockUserSession.swift b/ElementX/Sources/Services/Session/MockUserSession.swift index 0ab4ee9a7..af7306d15 100644 --- a/ElementX/Sources/Services/Session/MockUserSession.swift +++ b/ElementX/Sources/Services/Session/MockUserSession.swift @@ -25,5 +25,5 @@ struct MockUserSession: UserSessionProtocol { let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol - var sessionSecurityStatePublisher: AnyPublisher = CurrentValueSubject(.init(verificationState: .verified, recoveryState: .enabled)).eraseToAnyPublisher() + var sessionSecurityStatePublisher = CurrentValueSubject(.init(verificationState: .verified, recoveryState: .enabled)).asCurrentValuePublisher() } diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index b413fb689..1e49674f7 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -51,22 +51,16 @@ class UserSession: UserSessionProtocol { } } - let sessionSecurityStatePublisher: AnyPublisher + let sessionSecurityStateSubject = CurrentValueSubject(.init(verificationState: .unknown, recoveryState: .unknown)) + var sessionSecurityStatePublisher: CurrentValuePublisher { + sessionSecurityStateSubject.asCurrentValuePublisher() + } init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol) { self.clientProxy = clientProxy self.mediaProvider = mediaProvider self.voiceMessageMediaManager = voiceMessageMediaManager - 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 @@ -88,6 +82,18 @@ class UserSession: UserSessionProtocol { break } } + + 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) + .sink { [weak self] value in + self?.sessionSecurityStateSubject.send(value) + } + .store(in: &cancellables) } // MARK: - Private diff --git a/ElementX/Sources/Services/Session/UserSessionProtocol.swift b/ElementX/Sources/Services/Session/UserSessionProtocol.swift index b9939d0a9..4f0f572c9 100644 --- a/ElementX/Sources/Services/Session/UserSessionProtocol.swift +++ b/ElementX/Sources/Services/Session/UserSessionProtocol.swift @@ -42,7 +42,7 @@ protocol UserSessionProtocol { var mediaProvider: MediaProviderProtocol { get } var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get } - var sessionSecurityStatePublisher: AnyPublisher { get } + var sessionSecurityStatePublisher: CurrentValuePublisher { get } var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get } var callbacks: PassthroughSubject { get } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 60cea2a49..7f4536b46 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -552,7 +552,8 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .sessionVerification: var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5)) - let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy) + let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy, + recoveryState: .unknown) return SessionVerificationScreenCoordinator(parameters: parameters) case .userSessionScreen, .userSessionScreenReply, .userSessionScreenRTE: let appSettings: AppSettings = ServiceLocator.shared.settings diff --git a/UnitTests/Sources/SessionVerificationViewModelTests.swift b/UnitTests/Sources/SessionVerificationViewModelTests.swift index 81172814a..c17e27b42 100644 --- a/UnitTests/Sources/SessionVerificationViewModelTests.swift +++ b/UnitTests/Sources/SessionVerificationViewModelTests.swift @@ -27,7 +27,8 @@ class SessionVerificationViewModelTests: XCTestCase { override func setUpWithError() throws { sessionVerificationController = SessionVerificationControllerProxyMock.configureMock() - viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController) + viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController, + recoveryState: .incomplete) context = viewModel.context } diff --git a/UnitTests/__Snapshots__/PreviewTests/test_sessionVerification.Initial.png b/UnitTests/__Snapshots__/PreviewTests/test_sessionVerification.Initial.png index 1e98e6618..a6804271c 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_sessionVerification.Initial.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_sessionVerification.Initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0c8f14034cf3b7ec84ea309e02ef139ae9ef6556137c1c0a4348286faac3a8f -size 98718 +oid sha256:768819e156631bd2188b55376ce95e270a16828b0d70c3f2c3559a979e558d19 +size 105136