mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Key backup returns (#1951)
* Converge on UserSessionFlowCoordinator logout confirmation * Add logout confirmation screen strings to untranslated. * Fix chat backup learn more URl fragment. * Implement logout flows that check recovery and key backup for the last session * Move logout confirmation screen strings to localazy * Change encrypted timeline item copy to "Waiting for decryption key" * Use different encrypted history banner based on key backup states * Introduce a SettingsFlowCoordinator and implement navigation directly to the secure backup screen from the logout flows. * Fix **mocked** secure backup controller flows * Simplify encrypted history banner logic * Address PR comments
This commit is contained in:
@@ -106,9 +106,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
MXLog.error("[RoomFlowCoordinator] Failed to get member: RoomProxy is nil")
|
||||
}
|
||||
}
|
||||
case .invites:
|
||||
break
|
||||
case .genericCallLink, .oidcCallback:
|
||||
case .invites, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -347,7 +345,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
mediaPlayerProvider: mediaPlayerProvider,
|
||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager)
|
||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||
secureBackupController: userSession.clientProxy.secureBackupController)
|
||||
self.timelineController = timelineController
|
||||
|
||||
analytics.trackViewRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// Copyright 2023 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 Combine
|
||||
import Foundation
|
||||
|
||||
enum SettingsFlowCoordinatorAction {
|
||||
case presentedSettings
|
||||
case dismissedSettings
|
||||
case runLogoutFlow
|
||||
case clearCache
|
||||
}
|
||||
|
||||
struct SettingsFlowCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let appLockService: AppLockServiceProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
let notificationSettings: NotificationSettingsProxyProtocol
|
||||
let secureBackupController: SecureBackupControllerProtocol
|
||||
let appSettings: AppSettings
|
||||
let navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
}
|
||||
|
||||
class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let parameters: SettingsFlowCoordinatorParameters
|
||||
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
private var userIndicatorController: UserIndicatorControllerProtocol!
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SettingsFlowCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<SettingsFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: SettingsFlowCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
switch appRoute {
|
||||
case .settings:
|
||||
presentSettingsScreen(animated: animated)
|
||||
case .chatBackupSettings:
|
||||
if navigationStackCoordinator == nil {
|
||||
presentSettingsScreen(animated: animated)
|
||||
}
|
||||
|
||||
// The navigation stack doesn't like it if the root and the push happen
|
||||
// on the same loop run
|
||||
DispatchQueue.main.async {
|
||||
self.presentSecureBackupScreen(animated: animated)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) { }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func presentSettingsScreen(animated: Bool) {
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator)
|
||||
|
||||
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: userIndicatorController,
|
||||
userSession: parameters.userSession,
|
||||
appLockService: parameters.appLockService,
|
||||
bugReportService: parameters.bugReportService,
|
||||
notificationSettings: parameters.userSession.clientProxy.notificationSettings,
|
||||
secureBackupController: parameters.userSession.clientProxy.secureBackupController,
|
||||
appSettings: parameters.appSettings)
|
||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
|
||||
|
||||
settingsScreenCoordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.parameters.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
case .logout:
|
||||
self.parameters.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
// The settings sheet needs to be dismissed before the alert can be shown
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.actionsSubject.send(.runLogoutFlow)
|
||||
}
|
||||
case .clearCache:
|
||||
actionsSubject.send(.clearCache)
|
||||
case .secureBackup:
|
||||
presentSecureBackupScreen(animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(settingsScreenCoordinator, animated: animated)
|
||||
|
||||
self.parameters.navigationSplitCoordinator.setSheetCoordinator(userIndicatorController) { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
navigationStackCoordinator = nil
|
||||
userIndicatorController = nil
|
||||
actionsSubject.send(.dismissedSettings)
|
||||
}
|
||||
|
||||
actionsSubject.send(.presentedSettings)
|
||||
}
|
||||
|
||||
private func presentSecureBackupScreen(animated: Bool) {
|
||||
let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
|
||||
secureBackupController: parameters.userSession.clientProxy.secureBackupController,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: userIndicatorController))
|
||||
|
||||
navigationStackCoordinator.push(coordinator, animated: animated)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
enum UserSessionFlowCoordinatorAction {
|
||||
case signOut
|
||||
case logout
|
||||
case clearCache
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private let stateMachine: UserSessionFlowCoordinatorStateMachine
|
||||
private let roomFlowCoordinator: RoomFlowCoordinator
|
||||
private let settingsFlowCoordinator: SettingsFlowCoordinator
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var migrationCancellable: AnyCancellable?
|
||||
@@ -76,11 +77,20 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appSettings: appSettings,
|
||||
analytics: analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
|
||||
settingsFlowCoordinator = SettingsFlowCoordinator(parameters: .init(userSession: userSession,
|
||||
appLockService: appLockService,
|
||||
bugReportService: bugReportService,
|
||||
notificationSettings: userSession.clientProxy.notificationSettings,
|
||||
secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
appSettings: appSettings,
|
||||
navigationSplitCoordinator: navigationSplitCoordinator))
|
||||
|
||||
setupStateMachine()
|
||||
|
||||
roomFlowCoordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentedRoom(let roomID):
|
||||
analytics.signpost.beginRoomFlow(roomID)
|
||||
@@ -93,6 +103,22 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
settingsFlowCoordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentedSettings:
|
||||
stateMachine.processEvent(.showSettingsScreen)
|
||||
case .dismissedSettings:
|
||||
stateMachine.processEvent(.dismissedSettingsScreen)
|
||||
case .runLogoutFlow:
|
||||
runLogoutFlow()
|
||||
case .clearCache:
|
||||
actionsSubject.send(.clearCache)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@@ -131,6 +157,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
self.navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
|
||||
case .oidcCallback:
|
||||
break
|
||||
case .settings, .chatBackupSettings:
|
||||
settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +221,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.roomList, .showSettingsScreen, .settingsScreen):
|
||||
presentSettingsScreen(animated: animated)
|
||||
break
|
||||
case (.settingsScreen, .dismissedSettingsScreen, .roomList):
|
||||
break
|
||||
|
||||
@@ -211,7 +239,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
presentInvitesList(animated: animated)
|
||||
case (.invitesScreen, .showInvitesScreen, .invitesScreen):
|
||||
break
|
||||
case (.invitesScreen, .closedInvitesScreen, .roomList):
|
||||
case (.invitesScreen, .dismissedInvitesScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .showLogoutConfirmationScreen, .logoutConfirmationScreen):
|
||||
presentSecureBackupConfirmationScreen()
|
||||
case (.logoutConfirmationScreen, .dismissedLogoutConfirmationScreen, .roomList):
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -288,15 +321,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
roomFlowCoordinator.handleAppRoute(.roomList, animated: true)
|
||||
}
|
||||
case .presentSettingsScreen:
|
||||
stateMachine.processEvent(.showSettingsScreen)
|
||||
settingsFlowCoordinator.handleAppRoute(.settings, animated: true)
|
||||
case .presentFeedbackScreen:
|
||||
stateMachine.processEvent(.feedbackScreen)
|
||||
case .presentSessionVerificationScreen:
|
||||
stateMachine.processEvent(.showSessionVerificationScreen)
|
||||
case .presentStartChatScreen:
|
||||
stateMachine.processEvent(.showStartChatScreen)
|
||||
case .signOut:
|
||||
actionsSubject.send(.signOut)
|
||||
case .logout:
|
||||
runLogoutFlow()
|
||||
case .presentInvitesScreen:
|
||||
stateMachine.processEvent(.showInvitesScreen)
|
||||
}
|
||||
@@ -321,44 +354,44 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
private func presentSettingsScreen(animated: Bool) {
|
||||
let settingsNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
private func runLogoutFlow() {
|
||||
let secureBackupController = userSession.clientProxy.secureBackupController
|
||||
|
||||
let userIndicatorController = UserIndicatorController(rootCoordinator: settingsNavigationStackCoordinator)
|
||||
|
||||
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
|
||||
userIndicatorController: userIndicatorController,
|
||||
userSession: userSession,
|
||||
appLockService: appLockService,
|
||||
bugReportService: bugReportService,
|
||||
notificationSettings: userSession.clientProxy.notificationSettings,
|
||||
secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
appSettings: appSettings)
|
||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
|
||||
|
||||
settingsScreenCoordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
case .logout:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
actionsSubject.send(.signOut)
|
||||
case .clearCache:
|
||||
actionsSubject.send(.clearCache)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
settingsNavigationStackCoordinator.setRootCoordinator(settingsScreenCoordinator, animated: animated)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(userIndicatorController) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedSettingsScreen)
|
||||
guard secureBackupController.isLastSession, appSettings.chatBackupEnabled else {
|
||||
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
|
||||
title: L10n.screenSignoutRecoveryDisabledTitle,
|
||||
message: L10n.screenSignoutRecoveryDisabledSubtitle,
|
||||
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
|
||||
self?.actionsSubject.send(.logout)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
guard secureBackupController.recoveryKeyState.value == .enabled else {
|
||||
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
|
||||
title: L10n.screenSignoutRecoveryDisabledTitle,
|
||||
message: L10n.screenSignoutRecoveryDisabledSubtitle,
|
||||
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
|
||||
self?.actionsSubject.send(.logout)
|
||||
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
|
||||
self?.settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
guard secureBackupController.keyBackupState.value == .enabled else {
|
||||
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
|
||||
title: L10n.screenSignoutKeyBackupDisabledTitle,
|
||||
message: L10n.screenSignoutKeyBackupDisabledSubtitle,
|
||||
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
|
||||
self?.actionsSubject.send(.logout)
|
||||
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
|
||||
self?.settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
presentSecureBackupConfirmationScreen()
|
||||
}
|
||||
|
||||
// MARK: Session verification
|
||||
@@ -457,7 +490,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
|
||||
sidebarNavigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||
self?.stateMachine.processEvent(.closedInvitesScreen)
|
||||
self?.stateMachine.processEvent(.dismissedInvitesScreen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,4 +512,28 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(callScreenCoordinator, animated: true)
|
||||
}
|
||||
|
||||
// MARK: Secure backup confirmation
|
||||
|
||||
private func presentSecureBackupConfirmationScreen() {
|
||||
let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor))
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
case .settings:
|
||||
settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
|
||||
case .logout:
|
||||
actionsSubject.send(.logout)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(coordinator, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
/// Showing invites list screen
|
||||
case invitesScreen(selectedRoomID: String?)
|
||||
|
||||
// Showing the logout flows
|
||||
case logoutConfirmationScreen(selectedRoomID: String?)
|
||||
}
|
||||
|
||||
struct EventUserInfo {
|
||||
@@ -100,7 +103,12 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
/// Request presentation of the invites screen
|
||||
case showInvitesScreen
|
||||
/// The invites screen has been dismissed
|
||||
case closedInvitesScreen
|
||||
case dismissedInvitesScreen
|
||||
|
||||
/// Logout has been requested and this is the last sesion
|
||||
case showLogoutConfirmationScreen
|
||||
/// Logout has been cancelled
|
||||
case dismissedLogoutConfirmationScreen
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
@@ -157,12 +165,17 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
case (.showInvitesScreen, .invitesScreen(let selectedRoomID)):
|
||||
return .invitesScreen(selectedRoomID: selectedRoomID)
|
||||
|
||||
case (.closedInvitesScreen, .invitesScreen(let selectedRoomID)):
|
||||
case (.dismissedInvitesScreen, .invitesScreen(let selectedRoomID)):
|
||||
return .roomList(selectedRoomID: selectedRoomID)
|
||||
|
||||
case (.presentWelcomeScreen, .roomList):
|
||||
return .welcomeScreen
|
||||
|
||||
case (.showLogoutConfirmationScreen, .roomList(let selectedRoomID)):
|
||||
return .logoutConfirmationScreen(selectedRoomID: selectedRoomID)
|
||||
case (.dismissedLogoutConfirmationScreen, .logoutConfirmationScreen(let selectedRoomID)):
|
||||
return .roomList(selectedRoomID: selectedRoomID)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user