mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +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:
+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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user