mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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:
@@ -700,6 +700,7 @@
|
||||
748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; };
|
||||
74DF5BC17DE9F51E077FD457 /* LinkNewDeviceServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA257D747DD7E6FFA5C2BE2D /* LinkNewDeviceServiceMock.swift */; };
|
||||
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; };
|
||||
75072C529C9C363F531721F1 /* RecoveryKeyScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE97AD18893E2D8120F559D /* RecoveryKeyScreenHook.swift */; };
|
||||
754602A7B2AAD443C4228ED4 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
|
||||
755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */; };
|
||||
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; };
|
||||
@@ -2329,6 +2330,7 @@
|
||||
7ACFFE7814849BB200CE0969 /* SpaceAddRoomsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenModels.swift; sourceTree = "<group>"; };
|
||||
7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
7AE97AD18893E2D8120F559D /* RecoveryKeyScreenHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryKeyScreenHook.swift; sourceTree = "<group>"; };
|
||||
7AFD012C3A9F5EF276DDD4AA /* AnalyticsPromptScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
7B04BD3874D736127A8156B8 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
|
||||
@@ -3748,6 +3750,7 @@
|
||||
8B89D6C760E8CAE29CA28FB1 /* CompoundHook.swift */,
|
||||
0F60FDB3AEFC60830BD289FE /* DeveloperOptionsScreenHook.swift */,
|
||||
04CAC77224E949D8E758E2E3 /* OAuthPresenterHook.swift */,
|
||||
7AE97AD18893E2D8120F559D /* RecoveryKeyScreenHook.swift */,
|
||||
D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.swift */,
|
||||
B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */,
|
||||
2A95C9B8299A36A6495DECA6 /* TracingHook.swift */,
|
||||
@@ -8748,6 +8751,7 @@
|
||||
ABD29E06DD1224812E750AF8 /* ReadReceiptCell.swift in Sources */,
|
||||
1653275750CE11F5CE94DDFD /* ReadReceiptsSummaryView.swift in Sources */,
|
||||
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */,
|
||||
75072C529C9C363F531721F1 /* RecoveryKeyScreenHook.swift in Sources */,
|
||||
AF19D65A9C60C6B2646F3210 /* RedactedRoomTimelineItem.swift in Sources */,
|
||||
8CFDA5F1562479CB3A34D277 /* RedactedRoomTimelineView.swift in Sources */,
|
||||
3A2640C52505BAB3D983A92A /* RemotePreference.swift in Sources */,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
Submodule Enterprise updated: ee4aca0bd5...eaa8ac5101
Reference in New Issue
Block a user