mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Encryption Flow Coordinators. (#3471)
* Manage the secure backup screens with flow coordinators.
* Add UI tests for the EncryptionSettingsFlowCoordinator.
* Realise that the settings flow can't reset anymore and remove the sub-flow 🤦♂️
* Add UI tests for the EncryptionResetFlowCoordinator.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftState
|
||||
|
||||
enum EncryptionResetFlowCoordinatorAction: Equatable {
|
||||
/// The flow is complete.
|
||||
case resetComplete
|
||||
/// The flow was cancelled.
|
||||
case cancel
|
||||
}
|
||||
|
||||
struct EncryptionResetFlowCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let windowManger: WindowManagerProtocol
|
||||
}
|
||||
|
||||
class EncryptionResetFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
private let windowManager: WindowManagerProtocol
|
||||
|
||||
enum State: StateType {
|
||||
/// The state machine hasn't started.
|
||||
case initial
|
||||
/// The root screen for this flow.
|
||||
case encryptionResetScreen
|
||||
/// Confirming the user's password to continue.
|
||||
case confirmingPassword
|
||||
}
|
||||
|
||||
enum Event: EventType {
|
||||
/// The flow is being started.
|
||||
case start
|
||||
|
||||
/// The user needs to confirm their password to reset.
|
||||
case confirmPassword
|
||||
/// The user confirmed their password.
|
||||
case finishedConfirmingPassword
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
private let actionsSubject: PassthroughSubject<EncryptionResetFlowCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: EncryptionResetFlowCoordinatorParameters) {
|
||||
userSession = parameters.userSession
|
||||
userIndicatorController = parameters.userIndicatorController
|
||||
navigationStackCoordinator = parameters.navigationStackCoordinator
|
||||
windowManager = parameters.windowManger
|
||||
|
||||
stateMachine = .init(state: .initial)
|
||||
configureStateMachine()
|
||||
}
|
||||
|
||||
func start() {
|
||||
stateMachine.tryEvent(.start)
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
// There aren't any routes to this screen, so always clear the stack.
|
||||
clearRoute(animated: animated)
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
// As we push screens on top of an existing stack, popping to root wouldn't be safe.
|
||||
switch stateMachine.state {
|
||||
case .initial:
|
||||
break
|
||||
case .encryptionResetScreen:
|
||||
navigationStackCoordinator.pop(animated: animated)
|
||||
case .confirmingPassword:
|
||||
navigationStackCoordinator.pop(animated: animated) // Password screen.
|
||||
navigationStackCoordinator.pop(animated: animated) // EncryptionReset screen.
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func configureStateMachine() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .encryptionResetScreen]) { [weak self] _ in
|
||||
self?.presentEncryptionResetScreen()
|
||||
}
|
||||
|
||||
stateMachine.addRoutes(event: .confirmPassword, transitions: [.encryptionResetScreen => .confirmingPassword]) { [weak self] context in
|
||||
guard let passwordPublisher = context.userInfo as? PassthroughSubject<String, Never> else { fatalError("Expected a publisher in the userInfo.") }
|
||||
self?.presentPasswordScreen(passwordPublisher: passwordPublisher)
|
||||
}
|
||||
stateMachine.addRoutes(event: .finishedConfirmingPassword, transitions: [.confirmingPassword => .encryptionResetScreen])
|
||||
|
||||
stateMachine.addErrorHandler { context in
|
||||
fatalError("Unexpected transition: \(context)")
|
||||
}
|
||||
}
|
||||
|
||||
private func presentEncryptionResetScreen() {
|
||||
let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy,
|
||||
userIndicatorController: userIndicatorController))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
presentOIDCAuthorization(for: url)
|
||||
case .requestPassword(let passwordPublisher):
|
||||
stateMachine.tryEvent(.confirmPassword, userInfo: passwordPublisher)
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
case .resetFinished:
|
||||
actionsSubject.send(.resetComplete)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentPasswordScreen(passwordPublisher: PassthroughSubject<String, Never>) {
|
||||
let coordinator = EncryptionResetPasswordScreenCoordinator(parameters: .init(passwordPublisher: passwordPublisher))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .passwordEntered:
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator) { [stateMachine] in
|
||||
stateMachine.tryEvent(.finishedConfirmingPassword)
|
||||
}
|
||||
}
|
||||
|
||||
private var accountSettingsPresenter: OIDCAccountSettingsPresenter?
|
||||
private func presentOIDCAuthorization(for url: URL) {
|
||||
// Note to anyone in the future if you come back here to make this open in Safari instead of a WAS.
|
||||
// As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷♂️
|
||||
accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: windowManager.mainWindow)
|
||||
accountSettingsPresenter?.start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftState
|
||||
|
||||
enum EncryptionSettingsFlowCoordinatorAction: Equatable {
|
||||
/// The flow is complete.
|
||||
case complete
|
||||
}
|
||||
|
||||
struct EncryptionSettingsFlowCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let appSettings: AppSettings
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
}
|
||||
|
||||
class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator?
|
||||
|
||||
enum State: StateType {
|
||||
/// The state machine hasn't started.
|
||||
case initial
|
||||
/// The root screen for this flow.
|
||||
case secureBackupScreen
|
||||
/// The user is managing their recovery key.
|
||||
case recoveryKeyScreen
|
||||
/// The user is disabling key backups.
|
||||
case keyBackupScreen
|
||||
}
|
||||
|
||||
enum Event: EventType {
|
||||
/// The flow is being started.
|
||||
case start
|
||||
|
||||
/// The user would like to manage their recovery key.
|
||||
case manageRecoveryKey
|
||||
/// The user finished managing their recovery key.
|
||||
case finishedManagingRecoveryKey
|
||||
|
||||
/// The user doesn't want to use key backup any more.
|
||||
case disableKeyBackup
|
||||
/// The key backup screen was dismissed.
|
||||
case finishedDisablingKeyBackup
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
private let actionsSubject: PassthroughSubject<EncryptionSettingsFlowCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<EncryptionSettingsFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: EncryptionSettingsFlowCoordinatorParameters) {
|
||||
userSession = parameters.userSession
|
||||
appSettings = parameters.appSettings
|
||||
userIndicatorController = parameters.userIndicatorController
|
||||
navigationStackCoordinator = parameters.navigationStackCoordinator
|
||||
|
||||
stateMachine = .init(state: .initial)
|
||||
configureStateMachine()
|
||||
}
|
||||
|
||||
func start() {
|
||||
stateMachine.tryEvent(.start)
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
switch appRoute {
|
||||
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
|
||||
.roomDetails, .roomMemberDetails, .userProfile,
|
||||
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
|
||||
.call, .genericCallLink, .settings:
|
||||
// These routes aren't in this flow so clear the entire stack.
|
||||
clearRoute(animated: animated)
|
||||
case .chatBackupSettings:
|
||||
popToRootScreen(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
let fromState = stateMachine.state
|
||||
popToRootScreen(animated: animated)
|
||||
guard fromState != .initial else { return }
|
||||
navigationStackCoordinator.pop(animated: animated) // SecureBackup screen.
|
||||
}
|
||||
|
||||
func popToRootScreen(animated: Bool) {
|
||||
// As we push screens on top of an existing stack, a literal pop to root wouldn't be safe.
|
||||
switch stateMachine.state {
|
||||
case .initial, .secureBackupScreen:
|
||||
break
|
||||
case .recoveryKeyScreen:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // RecoveryKey screen.
|
||||
case .keyBackupScreen:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // KeyBackup screen.
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func configureStateMachine() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .secureBackupScreen]) { [weak self] _ in
|
||||
self?.presentSecureBackupScreen()
|
||||
}
|
||||
|
||||
stateMachine.addRoutes(event: .manageRecoveryKey, transitions: [.secureBackupScreen => .recoveryKeyScreen]) { [weak self] _ in
|
||||
self?.presentRecoveryKeyScreen()
|
||||
}
|
||||
stateMachine.addRoutes(event: .finishedManagingRecoveryKey, transitions: [.recoveryKeyScreen => .secureBackupScreen])
|
||||
|
||||
stateMachine.addRoutes(event: .disableKeyBackup, transitions: [.secureBackupScreen => .keyBackupScreen]) { [weak self] _ in
|
||||
self?.presentKeyBackupScreen()
|
||||
}
|
||||
stateMachine.addRoutes(event: .finishedDisablingKeyBackup, transitions: [.keyBackupScreen => .secureBackupScreen])
|
||||
|
||||
stateMachine.addErrorHandler { context in
|
||||
fatalError("Unexpected transition: \(context)")
|
||||
}
|
||||
}
|
||||
|
||||
private func presentSecureBackupScreen(animated: Bool = true) {
|
||||
let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: appSettings,
|
||||
clientProxy: userSession.clientProxy,
|
||||
userIndicatorController: userIndicatorController))
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .manageRecoveryKey:
|
||||
stateMachine.tryEvent(.manageRecoveryKey)
|
||||
case .disableKeyBackup:
|
||||
stateMachine.tryEvent(.disableKeyBackup)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||
self?.actionsSubject.send(.complete)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentRecoveryKeyScreen() {
|
||||
let sheetNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
userIndicatorController: userIndicatorController,
|
||||
isModallyPresented: true))
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .complete:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in
|
||||
stateMachine.tryEvent(.finishedManagingRecoveryKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentKeyBackupScreen() {
|
||||
let sheetNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let coordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
userIndicatorController: userIndicatorController))
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .done:
|
||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in
|
||||
stateMachine.tryEvent(.finishedDisablingKeyBackup)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// periphery: ignore - used to store the coordinator to avoid deallocation
|
||||
private var appLockFlowCoordinator: AppLockSetupFlowCoordinator?
|
||||
// periphery: ignore - used to store the coordinator to avoid deallocation
|
||||
private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator?
|
||||
|
||||
private let actionsSubject: PassthroughSubject<OnboardingFlowCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<OnboardingFlowCoordinatorAction, Never> {
|
||||
@@ -251,7 +253,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appSettings.hasRunIdentityConfirmationOnboarding = true
|
||||
stateMachine.tryEvent(.nextSkippingIdentityConfimed)
|
||||
case .reset:
|
||||
presentEncryptionResetScreen()
|
||||
startEncryptionResetFlow()
|
||||
case .logout:
|
||||
actionsSubject.send(.logout)
|
||||
}
|
||||
@@ -295,12 +297,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .recoveryFixed:
|
||||
case .complete:
|
||||
break // Moving to next state is Handled by the global session verification listener
|
||||
case .resetEncryption:
|
||||
presentEncryptionResetScreen()
|
||||
default:
|
||||
MXLog.error("Unexpected recovery action: \(action)")
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -308,31 +306,31 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
presentCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentEncryptionResetScreen() {
|
||||
private func startEncryptionResetFlow() {
|
||||
let resetNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy,
|
||||
navigationStackCoordinator: resetNavigationStackCoordinator,
|
||||
userIndicatorController: userIndicatorController))
|
||||
let coordinator = EncryptionResetFlowCoordinator(parameters: .init(userSession: userSession,
|
||||
userIndicatorController: userIndicatorController,
|
||||
navigationStackCoordinator: resetNavigationStackCoordinator,
|
||||
windowManger: windowManager))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
presentOIDCAuthorisationScreen(url: url)
|
||||
case .resetFinished:
|
||||
case .resetComplete:
|
||||
// Moving to next state is handled by the global session verification listener
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .cancel:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
resetNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
encryptionResetFlowCoordinator = coordinator
|
||||
coordinator.start()
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator)
|
||||
navigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator) { [weak self] in
|
||||
self?.encryptionResetFlowCoordinator = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func presentIdentityConfirmedScreen() {
|
||||
@@ -411,12 +409,4 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationStackCoordinator.push(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private var accountSettingsPresenter: OIDCAccountSettingsPresenter?
|
||||
private func presentOIDCAuthorisationScreen(url: URL) {
|
||||
// Note to anyone in the future if you come back here to make this open in Safari instead of a WAS.
|
||||
// As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷♂️
|
||||
accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: windowManager.mainWindow)
|
||||
accountSettingsPresenter?.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,10 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
|
||||
// periphery:ignore - retaining purpose
|
||||
private var encryptionSettingsFlowCoordinator: EncryptionSettingsFlowCoordinator?
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SettingsFlowCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<SettingsFlowCoordinatorAction, Never> {
|
||||
@@ -68,7 +69,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// The navigation stack doesn't like it if the root and the push happen
|
||||
// on the same loop run
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
self.presentSecureBackupScreen(animated: animated)
|
||||
self.startEncryptionSettingsFlow(animated: animated)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -102,7 +103,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
self.actionsSubject.send(.runLogoutFlow)
|
||||
}
|
||||
case .secureBackup:
|
||||
presentSecureBackupScreen(animated: true)
|
||||
startEncryptionSettingsFlow(animated: true)
|
||||
case .userDetails:
|
||||
presentUserDetailsEditScreen()
|
||||
case let .manageAccount(url):
|
||||
@@ -145,21 +146,22 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
actionsSubject.send(.presentedSettings)
|
||||
}
|
||||
|
||||
private func presentSecureBackupScreen(animated: Bool) {
|
||||
let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
|
||||
clientProxy: parameters.userSession.clientProxy,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: parameters.userIndicatorController))
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
private func startEncryptionSettingsFlow(animated: Bool) {
|
||||
let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: parameters.userSession,
|
||||
appSettings: parameters.appSettings,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
switch action {
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
self?.presentAccountManagementURL(url)
|
||||
case .complete:
|
||||
// The flow coordinator tidies up the stack, no need to do anything.
|
||||
self?.encryptionSettingsFlowCoordinator = nil
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator, animated: animated)
|
||||
encryptionSettingsFlowCoordinator = coordinator
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func presentUserDetailsEditScreen() {
|
||||
|
||||
@@ -13,6 +13,8 @@ struct ClientProxyMockConfiguration {
|
||||
var deviceID: String?
|
||||
var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init())
|
||||
var roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol?
|
||||
|
||||
var recoveryState: SecureBackupRecoveryState = .enabled
|
||||
}
|
||||
|
||||
enum ClientProxyMockError: Error {
|
||||
@@ -78,12 +80,8 @@ extension ClientProxyMock {
|
||||
loadMediaThumbnailForSourceWidthHeightThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
|
||||
loadMediaFileForSourceFilenameThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
|
||||
|
||||
secureBackupController = {
|
||||
let secureBackupController = SecureBackupControllerMock()
|
||||
secureBackupController.underlyingRecoveryState = .init(CurrentValueSubject<SecureBackupRecoveryState, Never>(.enabled))
|
||||
secureBackupController.underlyingKeyBackupState = .init(CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled))
|
||||
return secureBackupController
|
||||
}()
|
||||
secureBackupController = SecureBackupControllerMock(.init(recoveryState: configuration.recoveryState))
|
||||
resetIdentityReturnValue = .success(IdentityResetHandleSDKMock(.init()))
|
||||
|
||||
roomForIdentifierClosure = { [weak self] identifier in
|
||||
guard let room = self?.roomSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }) else {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension IdentityResetHandleSDKMock {
|
||||
struct Configuration { }
|
||||
|
||||
convenience init(_ configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
authTypeReturnValue = .uiaa
|
||||
resetAuthClosure = { _ in
|
||||
try await Task.sleep(for: .seconds(60))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
extension SecureBackupControllerMock {
|
||||
struct Configuration {
|
||||
var recoveryState: SecureBackupRecoveryState = .enabled
|
||||
var keyBackupState: SecureBackupKeyBackupState = .enabled
|
||||
}
|
||||
|
||||
convenience init(_ configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
let recoveryStateSubject = CurrentValueSubject<SecureBackupRecoveryState, Never>(configuration.recoveryState)
|
||||
underlyingRecoveryState = .init(recoveryStateSubject)
|
||||
|
||||
let keyBackupStateSubject = CurrentValueSubject<SecureBackupKeyBackupState, Never>(configuration.keyBackupState)
|
||||
underlyingKeyBackupState = .init(keyBackupStateSubject)
|
||||
|
||||
disableClosure = {
|
||||
recoveryStateSubject.send(.disabled)
|
||||
keyBackupStateSubject.send(.unknown)
|
||||
return .success(())
|
||||
}
|
||||
|
||||
enableClosure = {
|
||||
recoveryStateSubject.send(.disabled)
|
||||
keyBackupStateSubject.send(.enabled)
|
||||
return .success(())
|
||||
}
|
||||
|
||||
generateRecoveryKeyClosure = {
|
||||
recoveryStateSubject.send(.enabled)
|
||||
return .success("a1B2 C3d4 E5F6 g7H8 i9J0 K1l2 M3n4 O5p6 Q7R8 s9T0 U1v2 W3X4")
|
||||
}
|
||||
|
||||
confirmRecoveryKeyClosure = { _ in
|
||||
recoveryStateSubject.send(.enabled)
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ enum A11yIdentifiers {
|
||||
static let appLockSetupSettingsScreen = AppLockSetupSettingsScreen()
|
||||
static let bugReportScreen = BugReportScreen()
|
||||
static let changeServerScreen = ChangeServer()
|
||||
static let encryptionResetScreen = EncryptionResetScreen()
|
||||
static let encryptionResetPasswordScreen = EncryptionResetPasswordScreen()
|
||||
static let homeScreen = HomeScreen()
|
||||
static let loginScreen = LoginScreen()
|
||||
static let authenticationStartScreen = AuthenticationStartScreen()
|
||||
@@ -24,6 +26,9 @@ enum A11yIdentifiers {
|
||||
static let roomDetailsScreen = RoomDetailsScreen()
|
||||
static let roomNotificationSettingsScreen = RoomNotificationSettingsScreen()
|
||||
static let roomRolesAndPermissionsScreen = RoomRolesAndPermissionsScreen()
|
||||
static let secureBackupScreen = SecureBackupScreen()
|
||||
static let secureBackupKeyBackupScreen = SecureBackupKeyBackupScreen()
|
||||
static let secureBackupRecoveryKeyScreen = SecureBackupRecoveryKeyScreen()
|
||||
static let serverConfirmationScreen = ServerConfirmationScreen()
|
||||
static let sessionVerificationScreen = SessionVerificationScreen()
|
||||
static let settingsScreen = SettingsScreen()
|
||||
@@ -81,6 +86,15 @@ enum A11yIdentifiers {
|
||||
let dismiss = "change_server-dismiss"
|
||||
}
|
||||
|
||||
struct EncryptionResetScreen {
|
||||
let continueReset = "encryption_reset-continue_reset"
|
||||
}
|
||||
|
||||
struct EncryptionResetPasswordScreen {
|
||||
let passwordField = "encryption_reset_password-password_field"
|
||||
let submit = "encryption_reset_password-submit"
|
||||
}
|
||||
|
||||
struct HomeScreen {
|
||||
let userAvatar = "home_screen-user_avatar"
|
||||
let recoveryKeyConfirmationBannerContinue = "home_screen-recovery_key_confirmation_continue"
|
||||
@@ -178,6 +192,23 @@ enum A11yIdentifiers {
|
||||
let memberModeration = "room_roles_and_permissions-member_moderation"
|
||||
}
|
||||
|
||||
struct SecureBackupScreen {
|
||||
let keyStorage = "secure_backup-key_storage"
|
||||
let recoveryKey = "secure_backup-recovery_key"
|
||||
}
|
||||
|
||||
struct SecureBackupKeyBackupScreen {
|
||||
let deleteKeyStorage = "secure_backup_key_backup-delete_key_storage"
|
||||
}
|
||||
|
||||
struct SecureBackupRecoveryKeyScreen {
|
||||
let generateRecoveryKey = "secure_backup_recovery_key-generate_recovery_key"
|
||||
let copyRecoveryKey = "secure_backup_recovery_key-copy_recovery_key"
|
||||
let done = "secure_backup_recovery_key-done"
|
||||
let recoveryKeyField = "secure_backup_recovery_key-recovery_key_field"
|
||||
let confirm = "secure_backup_recovery_key-confirm"
|
||||
}
|
||||
|
||||
struct ServerConfirmationScreen {
|
||||
let `continue` = "server_confirmation-continue"
|
||||
let changeServer = "server_confirmation-change_server"
|
||||
|
||||
+8
-13
@@ -8,17 +8,12 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct EncryptionResetPasswordScreenCoordinatorParameters { }
|
||||
struct EncryptionResetPasswordScreenCoordinatorParameters {
|
||||
let passwordPublisher: PassthroughSubject<String, Never>
|
||||
}
|
||||
|
||||
enum EncryptionResetPasswordScreenCoordinatorAction: CustomStringConvertible {
|
||||
case resetIdentity(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .resetIdentity:
|
||||
"resetIdentity"
|
||||
}
|
||||
}
|
||||
enum EncryptionResetPasswordScreenCoordinatorAction {
|
||||
case passwordEntered
|
||||
}
|
||||
|
||||
final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -35,7 +30,7 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: EncryptionResetPasswordScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = EncryptionResetPasswordScreenViewModel()
|
||||
viewModel = EncryptionResetPasswordScreenViewModel(passwordPublisher: parameters.passwordPublisher)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@@ -44,8 +39,8 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .resetIdentity(let password):
|
||||
self.actionsSubject.send(.resetIdentity(password))
|
||||
case .passwordEntered:
|
||||
self.actionsSubject.send(.passwordEntered)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
+3
-10
@@ -7,15 +7,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum EncryptionResetPasswordScreenViewModelAction: CustomStringConvertible {
|
||||
case resetIdentity(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .resetIdentity:
|
||||
"resetIdentity"
|
||||
}
|
||||
}
|
||||
enum EncryptionResetPasswordScreenViewModelAction {
|
||||
case passwordEntered
|
||||
}
|
||||
|
||||
struct EncryptionResetPasswordScreenViewState: BindableState {
|
||||
@@ -28,5 +21,5 @@ struct EncryptionResetPasswordScreenViewStateBindings {
|
||||
}
|
||||
|
||||
enum EncryptionResetPasswordScreenViewAction {
|
||||
case resetIdentity
|
||||
case submit
|
||||
}
|
||||
|
||||
+8
-3
@@ -11,12 +11,16 @@ import SwiftUI
|
||||
typealias EncryptionResetPasswordScreenViewModelType = StateStoreViewModel<EncryptionResetPasswordScreenViewState, EncryptionResetPasswordScreenViewAction>
|
||||
|
||||
class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewModelType, EncryptionResetPasswordScreenViewModelProtocol {
|
||||
private let passwordPublisher: PassthroughSubject<String, Never>
|
||||
|
||||
private let actionsSubject: PassthroughSubject<EncryptionResetPasswordScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetPasswordScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
init(passwordPublisher: PassthroughSubject<String, Never>) {
|
||||
self.passwordPublisher = passwordPublisher
|
||||
|
||||
super.init(initialViewState: .init(bindings: .init(password: "")))
|
||||
}
|
||||
|
||||
@@ -26,8 +30,9 @@ class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewM
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .resetIdentity:
|
||||
actionsSubject.send(.resetIdentity(state.bindings.password))
|
||||
case .submit:
|
||||
passwordPublisher.send(state.bindings.password)
|
||||
actionsSubject.send(.passwordEntered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -5,6 +5,7 @@
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
@@ -32,9 +33,10 @@ struct EncryptionResetPasswordScreen: View {
|
||||
.padding(16)
|
||||
} bottomContent: {
|
||||
Button(L10n.actionResetIdentity, role: .destructive) {
|
||||
context.send(viewAction: .resetIdentity)
|
||||
context.send(viewAction: .submit)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.encryptionResetPasswordScreen.submit)
|
||||
}
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgCanvasDefault)
|
||||
@@ -58,8 +60,9 @@ struct EncryptionResetPasswordScreen: View {
|
||||
.focused($textFieldFocus)
|
||||
.submitLabel(.done)
|
||||
.onSubmit {
|
||||
context.send(viewAction: .resetIdentity)
|
||||
context.send(viewAction: .submit)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.encryptionResetPasswordScreen.passwordField)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +70,8 @@ struct EncryptionResetPasswordScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct EncryptionResetPasswordScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = EncryptionResetPasswordScreenViewModel()
|
||||
static let passwordPublisher = PassthroughSubject<String, Never>()
|
||||
static let viewModel = EncryptionResetPasswordScreenViewModel(passwordPublisher: passwordPublisher)
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
EncryptionResetPasswordScreen(context: viewModel.context)
|
||||
|
||||
+4
-23
@@ -9,14 +9,14 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
enum EncryptionResetScreenCoordinatorAction {
|
||||
case cancel
|
||||
case requestOIDCAuthorisation(URL)
|
||||
case requestPassword(passwordPublisher: PassthroughSubject<String, Never>)
|
||||
case resetFinished
|
||||
case cancel
|
||||
}
|
||||
|
||||
struct EncryptionResetScreenCoordinatorParameters {
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .requestPassword:
|
||||
presentPasswordScreen()
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
self.actionsSubject.send(.requestOIDCAuthorisation(url))
|
||||
case .requestPassword(let passwordPublisher):
|
||||
self.actionsSubject.send(.requestPassword(passwordPublisher: passwordPublisher))
|
||||
case .resetFinished:
|
||||
self.actionsSubject.send(.resetFinished)
|
||||
case .cancel:
|
||||
@@ -63,23 +63,4 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol {
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(EncryptionResetScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func presentPasswordScreen() {
|
||||
let coordinator = EncryptionResetPasswordScreenCoordinator(parameters: .init())
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .resetIdentity(let password):
|
||||
viewModel.continueResetFlowWith(password: password)
|
||||
parameters.navigationStackCoordinator.pop()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
parameters.navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,10 +5,11 @@
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum EncryptionResetScreenViewModelAction {
|
||||
case requestPassword
|
||||
case requestPassword(passwordPublisher: PassthroughSubject<String, Never>)
|
||||
case requestOIDCAuthorisation(url: URL)
|
||||
case resetFinished
|
||||
case cancel
|
||||
|
||||
+9
-7
@@ -21,6 +21,7 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp
|
||||
}
|
||||
|
||||
private var identityResetHandle: IdentityResetHandle?
|
||||
private var passwordCancellable: AnyCancellable?
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
@@ -46,12 +47,6 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp
|
||||
}
|
||||
}
|
||||
|
||||
func continueResetFlowWith(password: String) {
|
||||
Task {
|
||||
await resetWith(password: password)
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
Task {
|
||||
await identityResetHandle?.cancel()
|
||||
@@ -80,7 +75,14 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp
|
||||
|
||||
switch handle.authType() {
|
||||
case .uiaa:
|
||||
actionsSubject.send(.requestPassword)
|
||||
let passwordPublisher = PassthroughSubject<String, Never>()
|
||||
passwordCancellable = passwordPublisher.sink { [weak self] password in
|
||||
guard let self else { return }
|
||||
passwordCancellable = nil
|
||||
Task { await self.resetWith(password: password) }
|
||||
}
|
||||
|
||||
actionsSubject.send(.requestPassword(passwordPublisher: passwordPublisher))
|
||||
case .oidc(let oidcInfo):
|
||||
guard let url = URL(string: oidcInfo.approvalUrl) else {
|
||||
fatalError("Invalid URL received through identity reset handle: \(oidcInfo.approvalUrl)")
|
||||
|
||||
-1
@@ -12,6 +12,5 @@ protocol EncryptionResetScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetScreenViewModelAction, Never> { get }
|
||||
var context: EncryptionResetScreenViewModelType.Context { get }
|
||||
|
||||
func continueResetFlowWith(password: String)
|
||||
func stop()
|
||||
}
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ struct EncryptionResetScreen: View {
|
||||
context.send(viewAction: .reset)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.encryptionResetScreen.continueReset)
|
||||
}
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgSubtleSecondary)
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ struct SecureBackupKeyBackupScreen: View {
|
||||
Text(L10n.screenChatBackupKeyBackupActionDisable)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupKeyBackupScreen.deleteKeyStorage)
|
||||
}
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgCanvasDefault)
|
||||
|
||||
+20
-12
@@ -15,23 +15,22 @@ struct SecureBackupRecoveryKeyScreenCoordinatorParameters {
|
||||
}
|
||||
|
||||
enum SecureBackupRecoveryKeyScreenCoordinatorAction {
|
||||
case cancel
|
||||
case recoverySetUp
|
||||
case recoveryChanged
|
||||
case recoveryFixed
|
||||
case resetEncryption
|
||||
case complete
|
||||
}
|
||||
|
||||
final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters
|
||||
private var viewModel: SecureBackupRecoveryKeyScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<SecureBackupRecoveryKeyScreenCoordinatorAction, Never> = .init()
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SecureBackupRecoveryKeyScreenCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<SecureBackupRecoveryKeyScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
viewModel = SecureBackupRecoveryKeyScreenViewModel(secureBackupController: parameters.secureBackupController,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
isModallyPresented: parameters.isModallyPresented)
|
||||
@@ -44,20 +43,19 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
self.actionsSubject.send(.cancel)
|
||||
self.actionsSubject.send(.complete)
|
||||
case .done(let mode):
|
||||
switch mode {
|
||||
case .setupRecovery:
|
||||
self.actionsSubject.send(.recoverySetUp)
|
||||
showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess)
|
||||
case .changeRecovery:
|
||||
self.actionsSubject.send(.recoveryChanged)
|
||||
showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess)
|
||||
case .fixRecovery:
|
||||
self.actionsSubject.send(.recoveryFixed)
|
||||
showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess)
|
||||
case .unknown:
|
||||
fatalError()
|
||||
}
|
||||
case .resetEncryption:
|
||||
self.actionsSubject.send(.resetEncryption)
|
||||
self.actionsSubject.send(.complete)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -66,4 +64,14 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SecureBackupRecoveryKeyScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func showSuccessIndicator(title: String) {
|
||||
parameters.userIndicatorController.submitIndicator(.init(id: .init(),
|
||||
type: .modal(progress: .none, interactiveDismissDisabled: false, allowsInteraction: false),
|
||||
title: title,
|
||||
iconName: "checkmark",
|
||||
persistent: false))
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -10,7 +10,6 @@ import Foundation
|
||||
enum SecureBackupRecoveryKeyScreenViewModelAction {
|
||||
case done(mode: SecureBackupRecoveryKeyScreenViewMode)
|
||||
case cancel
|
||||
case resetEncryption
|
||||
}
|
||||
|
||||
enum SecureBackupRecoveryKeyScreenViewMode {
|
||||
@@ -82,7 +81,6 @@ enum SecureBackupRecoveryKeyScreenViewAction {
|
||||
case copyKey
|
||||
case keySaved
|
||||
case confirmKey
|
||||
case resetEncryption
|
||||
case done
|
||||
case cancel
|
||||
}
|
||||
|
||||
+3
-5
@@ -78,13 +78,11 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
state.bindings.alertInfo = .init(id: .init(),
|
||||
title: L10n.screenRecoveryKeySetupConfirmationTitle,
|
||||
message: L10n.screenRecoveryKeySetupConfirmationDescription,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
|
||||
secondaryButton: .init(title: L10n.actionContinue, action: { [weak self] in
|
||||
primaryButton: .init(title: L10n.actionContinue) { [weak self] in
|
||||
guard let self else { return }
|
||||
actionsSubject.send(.done(mode: context.viewState.mode))
|
||||
}))
|
||||
case .resetEncryption:
|
||||
actionsSubject.send(.resetEncryption)
|
||||
},
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -89,6 +89,7 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.disabled(context.confirmationRecoveryKey.isEmpty)
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.confirm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +112,7 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.disabled(context.viewState.recoveryKey == nil || context.viewState.doneButtonEnabled == false)
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.done)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +142,7 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
}
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.padding(.vertical, 11)
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.generateRecoveryKey)
|
||||
} else {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView()
|
||||
@@ -163,6 +166,7 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
}
|
||||
.tint(.compound.iconSecondary)
|
||||
.accessibilityLabel(L10n.actionCopy)
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.copyRecoveryKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +208,7 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
.onSubmit {
|
||||
context.send(viewAction: .confirmKey)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.recoveryKeyField)
|
||||
|
||||
if let subtitle = context.viewState.recoveryKeySubtitle {
|
||||
Text(subtitle)
|
||||
|
||||
+6
-86
@@ -11,12 +11,12 @@ import SwiftUI
|
||||
struct SecureBackupScreenCoordinatorParameters {
|
||||
let appSettings: AppSettings
|
||||
let clientProxy: ClientProxyProtocol
|
||||
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum SecureBackupScreenCoordinatorAction {
|
||||
case requestOIDCAuthorisation(URL)
|
||||
case manageRecoveryKey
|
||||
case disableKeyBackup
|
||||
}
|
||||
|
||||
final class SecureBackupScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -43,53 +43,10 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .recoveryKey:
|
||||
let recoveryNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let recoveryKeyCoordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
isModallyPresented: true))
|
||||
|
||||
recoveryKeyCoordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
case .recoverySetUp:
|
||||
showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess)
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
case .recoveryChanged:
|
||||
showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess)
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
case .recoveryFixed:
|
||||
showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess)
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
case .resetEncryption:
|
||||
showEncryptionReset(recoveryNavigationStackCoordinator: recoveryNavigationStackCoordinator)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
recoveryNavigationStackCoordinator.setRootCoordinator(recoveryKeyCoordinator, animated: true)
|
||||
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(recoveryNavigationStackCoordinator)
|
||||
case .keyBackup:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let keyBackupCoordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController,
|
||||
userIndicatorController: parameters.userIndicatorController))
|
||||
|
||||
keyBackupCoordinator.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .done:
|
||||
self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(keyBackupCoordinator, animated: true)
|
||||
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(navigationStackCoordinator)
|
||||
case .manageRecoveryKey:
|
||||
actionsSubject.send(.manageRecoveryKey)
|
||||
case .disableKeyBackup:
|
||||
actionsSubject.send(.disableKeyBackup)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -98,41 +55,4 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol {
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SecureBackupScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func showSuccessIndicator(title: String) {
|
||||
parameters.userIndicatorController.submitIndicator(.init(id: .init(),
|
||||
type: .modal(progress: .none, interactiveDismissDisabled: false, allowsInteraction: false),
|
||||
title: title,
|
||||
iconName: "checkmark",
|
||||
persistent: false))
|
||||
}
|
||||
|
||||
private func showEncryptionReset(recoveryNavigationStackCoordinator: NavigationStackCoordinator) {
|
||||
let resetNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: parameters.clientProxy,
|
||||
navigationStackCoordinator: resetNavigationStackCoordinator,
|
||||
userIndicatorController: parameters.userIndicatorController))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
recoveryNavigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
actionsSubject.send(.requestOIDCAuthorisation(url))
|
||||
case .resetFinished:
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil) // Dismiss the recovery screen
|
||||
recoveryNavigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
resetNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
recoveryNavigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
import Foundation
|
||||
|
||||
enum SecureBackupScreenViewModelAction {
|
||||
case recoveryKey
|
||||
case keyBackup
|
||||
case manageRecoveryKey
|
||||
case disableKeyBackup
|
||||
}
|
||||
|
||||
struct SecureBackupScreenViewState: BindableState {
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
|
||||
override func process(viewAction: SecureBackupScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .recoveryKey:
|
||||
actionsSubject.send(.recoveryKey)
|
||||
actionsSubject.send(.manageRecoveryKey)
|
||||
case .keyStorageToggled(let enable):
|
||||
let keyBackupState = secureBackupController.keyBackupState.value
|
||||
switch (keyBackupState, enable) {
|
||||
@@ -57,7 +57,7 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
|
||||
enableBackup()
|
||||
case (.enabled, false):
|
||||
state.bindings.keyStorageEnabled = keyBackupState.keyStorageToggleState // Reset the toggle in case the user cancels
|
||||
actionsSubject.send(.keyBackup)
|
||||
actionsSubject.send(.disableKeyBackup)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
+10
-10
@@ -53,7 +53,13 @@ struct SecureBackupScreen: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
})
|
||||
|
||||
keyStorageToggle
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupKeyStorageToggleTitle,
|
||||
description: context.viewState.keyStorageToggleDescription),
|
||||
kind: .toggle($context.keyStorageEnabled))
|
||||
.onChange(of: context.keyStorageEnabled) { _, newValue in
|
||||
context.send(viewAction: .keyStorageToggled(newValue))
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.keyStorage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +73,6 @@ struct SecureBackupScreen: View {
|
||||
return description
|
||||
}
|
||||
|
||||
private var keyStorageToggle: some View {
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupKeyStorageToggleTitle,
|
||||
description: context.viewState.keyStorageToggleDescription),
|
||||
kind: .toggle($context.keyStorageEnabled))
|
||||
.onChange(of: context.keyStorageEnabled) { _, newValue in
|
||||
context.send(viewAction: .keyStorageToggled(newValue))
|
||||
}
|
||||
}
|
||||
|
||||
private var recoveryKeySection: some View {
|
||||
Section {
|
||||
switch context.viewState.recoveryState {
|
||||
@@ -85,6 +82,7 @@ struct SecureBackupScreen: View {
|
||||
icon: \.key,
|
||||
iconAlignment: .top),
|
||||
kind: .navigationLink { context.send(viewAction: .recoveryKey) })
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey)
|
||||
case .disabled:
|
||||
ListRow(label: .default(title: L10n.screenChatBackupRecoveryActionSetup,
|
||||
description: L10n.screenChatBackupRecoveryActionChangeDescription,
|
||||
@@ -92,10 +90,12 @@ struct SecureBackupScreen: View {
|
||||
iconAlignment: .top),
|
||||
details: .icon(BadgeView(size: 10)),
|
||||
kind: .navigationLink { context.send(viewAction: .recoveryKey) })
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey)
|
||||
case .incomplete:
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionConfirm),
|
||||
details: .icon(BadgeView(size: 10)),
|
||||
kind: .navigationLink { context.send(viewAction: .recoveryKey) })
|
||||
.accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey)
|
||||
default:
|
||||
ListRow(label: .plain(title: L10n.commonLoading), details: .isWaiting(true), kind: .label)
|
||||
}
|
||||
|
||||
@@ -630,6 +630,40 @@ class MockScreen: Identifiable {
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .encryptionSettings, .encryptionSettingsOutOfSync:
|
||||
let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState))
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||
|
||||
let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: userSession,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
retainedState.append(coordinator)
|
||||
coordinator.start()
|
||||
|
||||
return navigationStackCoordinator
|
||||
case .encryptionReset:
|
||||
let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState))
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
let userIndicatorController = UserIndicatorController()
|
||||
userIndicatorController.window = windowManager.overlayWindow
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let coordinator = EncryptionResetFlowCoordinator(parameters: .init(userSession: userSession,
|
||||
userIndicatorController: userIndicatorController,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
windowManger: windowManager))
|
||||
|
||||
retainedState.append(coordinator)
|
||||
coordinator.start()
|
||||
|
||||
return navigationStackCoordinator
|
||||
case .autoUpdatingTimeline:
|
||||
let appSettings: AppSettings = ServiceLocator.shared.settings
|
||||
|
||||
@@ -20,6 +20,9 @@ enum UITestsScreenIdentifier: String {
|
||||
case createPoll
|
||||
case createRoom
|
||||
case createRoomNoUsers
|
||||
case encryptionSettings
|
||||
case encryptionSettingsOutOfSync
|
||||
case encryptionReset
|
||||
case roomLayoutBottom
|
||||
case roomLayoutMiddle
|
||||
case roomLayoutTop
|
||||
|
||||
Reference in New Issue
Block a user