mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Add a recovery key screen hook. (#5684)
update generateRecoveryKey to take a passphrase option Wire up AppHooks with recovery key hook Update xcodeproj Review fixups Review fixups Make the `SecureBackupRecoveryState.viewMode` extension private Bump the enterprise submodule
This commit is contained in:
@@ -68,6 +68,15 @@ final class AppHooks: AppHooksProtocol {
|
||||
func registerDeveloperOptionsScreenHook(_ hook: DeveloperOptionsScreenHookProtocol) {
|
||||
_developerOptionsScreenHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _recoveryKeyScreenHook: Mutex<RecoveryKeyScreenHookProtocol> = Mutex(DefaultRecoveryKeyScreenHook())
|
||||
var recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol {
|
||||
_recoveryKeyScreenHook.withLock { $0 }
|
||||
}
|
||||
|
||||
func registerRecoveryKeyScreenHook(_ hook: RecoveryKeyScreenHookProtocol) {
|
||||
_recoveryKeyScreenHook.withLock { $0 = hook }
|
||||
}
|
||||
#endif
|
||||
|
||||
private let _tracingHook: Mutex<TracingHookProtocol> = Mutex(DefaultTracingHook())
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@MainActor protocol SecureBackupRecoveryKeyCoordinatorProtocol: CoordinatorProtocol {
|
||||
var actions: AnyPublisher<SecureBackupRecoveryKeyScreenCoordinatorAction, Never> { get }
|
||||
}
|
||||
|
||||
protocol RecoveryKeyScreenHookProtocol {
|
||||
@MainActor func makeCoordinator(parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters, homeserver: String) -> any SecureBackupRecoveryKeyCoordinatorProtocol
|
||||
}
|
||||
|
||||
struct DefaultRecoveryKeyScreenHook: RecoveryKeyScreenHookProtocol {
|
||||
@MainActor func makeCoordinator(parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters, homeserver: String) -> any SecureBackupRecoveryKeyCoordinatorProtocol {
|
||||
SecureBackupRecoveryKeyScreenCoordinator(parameters: parameters)
|
||||
}
|
||||
}
|
||||
@@ -672,7 +672,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
isModallyPresented: true)
|
||||
|
||||
let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: parameters)
|
||||
let coordinator = flowParameters.appHooks.recoveryKeyScreenHook.makeCoordinator(parameters: parameters,
|
||||
homeserver: userSession.clientProxy.homeserver)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
|
||||
@@ -18,6 +18,7 @@ enum EncryptionSettingsFlowCoordinatorAction: Equatable {
|
||||
struct EncryptionSettingsFlowCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let appSettings: AppSettings
|
||||
let appHooks: AppHooks
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
}
|
||||
@@ -25,6 +26,7 @@ struct EncryptionSettingsFlowCoordinatorParameters {
|
||||
class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let appHooks: AppHooks
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
@@ -65,6 +67,7 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
init(parameters: EncryptionSettingsFlowCoordinatorParameters) {
|
||||
userSession = parameters.userSession
|
||||
appSettings = parameters.appSettings
|
||||
appHooks = parameters.appHooks
|
||||
userIndicatorController = parameters.userIndicatorController
|
||||
navigationStackCoordinator = parameters.navigationStackCoordinator
|
||||
|
||||
@@ -158,9 +161,11 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func presentRecoveryKeyScreen() {
|
||||
let sheetNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
userIndicatorController: userIndicatorController,
|
||||
isModallyPresented: true))
|
||||
let parameters = SecureBackupRecoveryKeyScreenCoordinatorParameters(secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
userIndicatorController: userIndicatorController,
|
||||
isModallyPresented: true)
|
||||
let coordinator = appHooks.recoveryKeyScreenHook.makeCoordinator(parameters: parameters,
|
||||
homeserver: userSession.clientProxy.homeserver)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -302,7 +302,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userIndicatorController: userIndicatorController,
|
||||
isModallyPresented: false)
|
||||
|
||||
let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: parameters)
|
||||
let coordinator = appHooks.recoveryKeyScreenHook.makeCoordinator(parameters: parameters,
|
||||
homeserver: userSession.clientProxy.homeserver)
|
||||
|
||||
coordinator.actions
|
||||
.sink { action in
|
||||
|
||||
@@ -143,6 +143,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private func startEncryptionSettingsFlow(animated: Bool) {
|
||||
let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: flowParameters.userSession,
|
||||
appSettings: flowParameters.appSettings,
|
||||
appHooks: flowParameters.appHooks,
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
|
||||
@@ -10963,30 +10963,44 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol, @unchecked Sen
|
||||
}
|
||||
//MARK: - generateRecoveryKey
|
||||
|
||||
private let generateRecoveryKeyCallsCountLock = NSLock()
|
||||
private var generateRecoveryKeyUnderlyingCallsCount = 0
|
||||
var generateRecoveryKeyCallsCount: Int {
|
||||
get { generateRecoveryKeyCallsCountLock.withLock { generateRecoveryKeyUnderlyingCallsCount } }
|
||||
set { generateRecoveryKeyCallsCountLock.withLock { generateRecoveryKeyUnderlyingCallsCount = newValue } }
|
||||
private let generateRecoveryKeyWithPassphraseCallsCountLock = NSLock()
|
||||
private var generateRecoveryKeyWithPassphraseUnderlyingCallsCount = 0
|
||||
var generateRecoveryKeyWithPassphraseCallsCount: Int {
|
||||
get { generateRecoveryKeyWithPassphraseCallsCountLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingCallsCount } }
|
||||
set { generateRecoveryKeyWithPassphraseCallsCountLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var generateRecoveryKeyCalled: Bool {
|
||||
return generateRecoveryKeyCallsCount > 0
|
||||
var generateRecoveryKeyWithPassphraseCalled: Bool {
|
||||
return generateRecoveryKeyWithPassphraseCallsCount > 0
|
||||
}
|
||||
private let generateRecoveryKeyWithPassphraseReceivedPassphraseLock = NSLock()
|
||||
private var generateRecoveryKeyWithPassphraseUnderlyingReceivedPassphrase: String?
|
||||
var generateRecoveryKeyWithPassphraseReceivedPassphrase: String? {
|
||||
get { generateRecoveryKeyWithPassphraseReceivedPassphraseLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingReceivedPassphrase } }
|
||||
set { generateRecoveryKeyWithPassphraseReceivedPassphraseLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingReceivedPassphrase = newValue } }
|
||||
}
|
||||
private let generateRecoveryKeyWithPassphraseReceivedInvocationsLock = NSLock()
|
||||
private var generateRecoveryKeyWithPassphraseUnderlyingReceivedInvocations: [String?] = []
|
||||
var generateRecoveryKeyWithPassphraseReceivedInvocations: [String?] {
|
||||
get { generateRecoveryKeyWithPassphraseReceivedInvocationsLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingReceivedInvocations } }
|
||||
set { generateRecoveryKeyWithPassphraseReceivedInvocationsLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingReceivedInvocations = newValue } }
|
||||
}
|
||||
|
||||
private let generateRecoveryKeyReturnValueLock = NSLock()
|
||||
private var generateRecoveryKeyUnderlyingReturnValue: Result<String, SecureBackupControllerError>!
|
||||
var generateRecoveryKeyReturnValue: Result<String, SecureBackupControllerError>! {
|
||||
get { generateRecoveryKeyReturnValueLock.withLock { generateRecoveryKeyUnderlyingReturnValue } }
|
||||
set { generateRecoveryKeyReturnValueLock.withLock { generateRecoveryKeyUnderlyingReturnValue = newValue } }
|
||||
private let generateRecoveryKeyWithPassphraseReturnValueLock = NSLock()
|
||||
private var generateRecoveryKeyWithPassphraseUnderlyingReturnValue: Result<String, SecureBackupControllerError>!
|
||||
var generateRecoveryKeyWithPassphraseReturnValue: Result<String, SecureBackupControllerError>! {
|
||||
get { generateRecoveryKeyWithPassphraseReturnValueLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingReturnValue } }
|
||||
set { generateRecoveryKeyWithPassphraseReturnValueLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingReturnValue = newValue } }
|
||||
}
|
||||
var generateRecoveryKeyClosure: (() async -> Result<String, SecureBackupControllerError>)?
|
||||
var generateRecoveryKeyWithPassphraseClosure: ((String?) async -> Result<String, SecureBackupControllerError>)?
|
||||
|
||||
func generateRecoveryKey() async -> Result<String, SecureBackupControllerError> {
|
||||
generateRecoveryKeyCallsCountLock.withLock { generateRecoveryKeyUnderlyingCallsCount += 1 }
|
||||
if let generateRecoveryKeyClosure = generateRecoveryKeyClosure {
|
||||
return await generateRecoveryKeyClosure()
|
||||
func generateRecoveryKey(withPassphrase passphrase: String?) async -> Result<String, SecureBackupControllerError> {
|
||||
generateRecoveryKeyWithPassphraseCallsCountLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingCallsCount += 1 }
|
||||
generateRecoveryKeyWithPassphraseReceivedPassphrase = passphrase
|
||||
generateRecoveryKeyWithPassphraseReceivedInvocationsLock.withLock { generateRecoveryKeyWithPassphraseUnderlyingReceivedInvocations.append(passphrase) }
|
||||
if let generateRecoveryKeyWithPassphraseClosure = generateRecoveryKeyWithPassphraseClosure {
|
||||
return await generateRecoveryKeyWithPassphraseClosure(passphrase)
|
||||
} else {
|
||||
return generateRecoveryKeyReturnValue
|
||||
return generateRecoveryKeyWithPassphraseReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - confirmRecoveryKey
|
||||
|
||||
@@ -36,7 +36,7 @@ extension SecureBackupControllerMock {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
generateRecoveryKeyClosure = {
|
||||
generateRecoveryKeyWithPassphraseClosure = { _ in
|
||||
recoveryStateSubject.send(.enabled)
|
||||
return .success("a1B2 C3d4 E5F6 g7H8 i9J0 K1l2 M3n4 O5p6 Q7R8 s9T0 U1v2 W3X4")
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ enum SecureBackupRecoveryKeyScreenCoordinatorAction {
|
||||
case complete
|
||||
}
|
||||
|
||||
final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
|
||||
final class SecureBackupRecoveryKeyScreenCoordinator: SecureBackupRecoveryKeyCoordinatorProtocol {
|
||||
private let parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters
|
||||
private var viewModel: SecureBackupRecoveryKeyScreenViewModelProtocol
|
||||
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
state.isGeneratingKey = true
|
||||
|
||||
Task {
|
||||
switch await secureBackupController.generateRecoveryKey() {
|
||||
switch await secureBackupController.generateRecoveryKey(withPassphrase: nil) {
|
||||
case .success(let key):
|
||||
state.recoveryKey = key
|
||||
case .failure(let error):
|
||||
@@ -101,7 +101,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
}
|
||||
}
|
||||
|
||||
extension SecureBackupRecoveryState {
|
||||
private extension SecureBackupRecoveryState {
|
||||
var viewMode: SecureBackupRecoveryKeyScreenViewMode {
|
||||
switch self {
|
||||
case .disabled:
|
||||
|
||||
+2
-2
@@ -278,9 +278,9 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview
|
||||
backupController.recoveryState = CurrentValueSubject<SecureBackupRecoveryState, Never>(recoveryState).asCurrentValuePublisher()
|
||||
|
||||
if let key {
|
||||
backupController.generateRecoveryKeyReturnValue = .success(key)
|
||||
backupController.generateRecoveryKeyWithPassphraseReturnValue = .success(key)
|
||||
} else {
|
||||
backupController.generateRecoveryKeyClosure = {
|
||||
backupController.generateRecoveryKeyWithPassphraseClosure = { _ in
|
||||
try? await Task.sleep(for: .seconds(1000))
|
||||
return .success("youshouldntseeme")
|
||||
}
|
||||
|
||||
@@ -110,19 +110,18 @@ class SecureBackupController: SecureBackupControllerProtocol {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func generateRecoveryKey() async -> Result<String, SecureBackupControllerError> {
|
||||
func generateRecoveryKey(withPassphrase passphrase: String?) async -> Result<String, SecureBackupControllerError> {
|
||||
do {
|
||||
guard recoveryState.value == .disabled else {
|
||||
if recoveryState.value == .disabled {
|
||||
MXLog.info("Enabling recovery")
|
||||
} else {
|
||||
MXLog.info("Resetting recovery key")
|
||||
|
||||
let key = try await encryption.resetRecoveryKey()
|
||||
return .success(key)
|
||||
}
|
||||
|
||||
MXLog.info("Enabling recovery")
|
||||
|
||||
var keyUploadErrored = false
|
||||
let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, passphrase: nil, progressListener: SDKListener { [weak self] state in
|
||||
// Note: `enableRecovery` also handles the reset case (equivalent to `resetRecoveryKey`).
|
||||
// Despite the name, it is the correct call for both initial setup and key rotation.
|
||||
let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, passphrase: passphrase, progressListener: SDKListener { [weak self] state in
|
||||
guard let self else { return }
|
||||
|
||||
switch state {
|
||||
|
||||
@@ -55,7 +55,7 @@ protocol SecureBackupControllerProtocol {
|
||||
func enable() async -> Result<Void, SecureBackupControllerError>
|
||||
func disable() async -> Result<Void, SecureBackupControllerError>
|
||||
|
||||
func generateRecoveryKey() async -> Result<String, SecureBackupControllerError>
|
||||
func generateRecoveryKey(withPassphrase passphrase: String?) async -> Result<String, SecureBackupControllerError>
|
||||
func confirmRecoveryKey(_ key: String) async -> Result<Void, SecureBackupControllerError>
|
||||
|
||||
func waitForKeyBackupUpload(uploadStateSubject: CurrentValueSubject<SecureBackupSteadyState, Never>) async -> Result<Void, SecureBackupControllerError>
|
||||
|
||||
@@ -748,6 +748,7 @@ class MockScreen: Identifiable {
|
||||
|
||||
let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: userSession,
|
||||
appSettings: appSettings,
|
||||
appHooks: AppHooks(),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
retainedState.append(coordinator)
|
||||
|
||||
Reference in New Issue
Block a user