Key backup Rust API adoption (#1952)

* Adopt Rust side secure backup APIs (incomplete, based on poljar/backup-pr)

* Implement the home screen recovery key confirmation banner.

* Update with the latest Rust changes.

The loading button for .unknown was incorrect. It should be treated as disabled.

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Stefan Ceriu
2023-10-27 22:16:51 +03:00
committed by GitHub
parent 4077f5f4bb
commit a837ce5cff
22 changed files with 444 additions and 166 deletions
+8
View File
@@ -319,6 +319,7 @@
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; };
5770C4906668C6D3008A2AC9 /* SessionVerificationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */; }; 5770C4906668C6D3008A2AC9 /* SessionVerificationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */; };
5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */; }; 5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */; };
584590D0EA548152A393E72C /* HomeScreenSessionVerificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E71C28CF29CD05B6D6AE8580 /* HomeScreenSessionVerificationBanner.swift */; };
588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; }; 588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; };
5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */; }; 5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */; };
5897A59DDBD3592282092223 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; }; 5897A59DDBD3592282092223 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; };
@@ -667,6 +668,7 @@
AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */; }; AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */; };
AFA1F2543DFF7B45DF68ACD6 /* CompletionSuggestionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170BF6F7923A5C3792442F27 /* CompletionSuggestionModels.swift */; }; AFA1F2543DFF7B45DF68ACD6 /* CompletionSuggestionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170BF6F7923A5C3792442F27 /* CompletionSuggestionModels.swift */; };
AFC518DCC38B821537EBF549 /* CreatePollScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */; }; AFC518DCC38B821537EBF549 /* CreatePollScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */; };
B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */; };
B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; }; B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; };
B09DC6E3D0EE87C4D4ABFAB3 /* EncryptedHistoryRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */; }; B09DC6E3D0EE87C4D4ABFAB3 /* EncryptedHistoryRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */; };
B0CB16349B96262AA65A04AF /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; }; B0CB16349B96262AA65A04AF /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; };
@@ -1040,6 +1042,7 @@
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; }; 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = "<group>"; }; 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = "<group>"; };
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = "<group>"; }; 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = "<group>"; };
05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = "<group>"; };
05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = "<group>"; }; 05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = "<group>"; };
05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = "<group>"; }; 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = "<group>"; };
0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -1832,6 +1835,7 @@
E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = "<group>"; }; E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = "<group>"; };
E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxy.swift; sourceTree = "<group>"; }; E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxy.swift; sourceTree = "<group>"; };
E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactory.swift; sourceTree = "<group>"; }; E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactory.swift; sourceTree = "<group>"; };
E71C28CF29CD05B6D6AE8580 /* HomeScreenSessionVerificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenSessionVerificationBanner.swift; sourceTree = "<group>"; };
E80F9E9B93B6ECE9A937B1C6 /* FormRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormRow.swift; sourceTree = "<group>"; }; E80F9E9B93B6ECE9A937B1C6 /* FormRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormRow.swift; sourceTree = "<group>"; };
E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = "<group>"; }; E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = "<group>"; };
@@ -2865,8 +2869,10 @@
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */, B902EA6CD3296B0E10EE432B /* HomeScreen.swift */,
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */, C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */,
24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */, 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */,
05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */,
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */, ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */,
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */, C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */,
E71C28CF29CD05B6D6AE8580 /* HomeScreenSessionVerificationBanner.swift */,
F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */, F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */,
); );
path = View; path = View;
@@ -5447,8 +5453,10 @@
77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */, 77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */,
64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */, 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */,
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */, 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */,
B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */,
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */, 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */,
A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */, A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */,
584590D0EA548152A393E72C /* HomeScreenSessionVerificationBanner.swift in Sources */,
D3986615892E7CF05C86518A /* HomeScreenUserMenuButton.swift in Sources */, D3986615892E7CF05C86518A /* HomeScreenUserMenuButton.swift in Sources */,
DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */, DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */,
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */, 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */,
@@ -172,6 +172,8 @@
"common_waiting_for_decryption_key" = "Waiting for decryption key"; "common_waiting_for_decryption_key" = "Waiting for decryption key";
"common_poll_end_confirmation" = "Are you sure you want to end this poll?"; "common_poll_end_confirmation" = "Are you sure you want to end this poll?";
"common_poll_summary" = "Poll: %1$@"; "common_poll_summary" = "Poll: %1$@";
"confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup.";
"confirm_recovery_key_banner_title" = "Confirm your recovery key";
"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; "crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
"dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings."; "dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings.";
"dialog_permission_generic" = "Please grant the permission in the system settings."; "dialog_permission_generic" = "Please grant the permission in the system settings.";
@@ -196,7 +198,6 @@
"error_failed_loading_messages" = "Failed loading messages"; "error_failed_loading_messages" = "Failed loading messages";
"error_failed_locating_user" = "%1$@ could not access your location. Please try again later."; "error_failed_locating_user" = "%1$@ could not access your location. Please try again later.";
"error_failed_uploading_voice_message" = "Failed to upload your voice message."; "error_failed_uploading_voice_message" = "Failed to upload your voice message.";
"error_missing_location_auth_ios" = "%1$@ does not have permission to access your location. You can enable access in Settings > Location";
"error_no_compatible_app_found" = "No compatible app was found to handle this action."; "error_no_compatible_app_found" = "No compatible app was found to handle this action.";
"error_some_messages_have_not_been_sent" = "Some messages have not been sent"; "error_some_messages_have_not_been_sent" = "Some messages have not been sent";
"error_unknown" = "Sorry, an error occurred"; "error_unknown" = "Sorry, an error occurred";
@@ -64,7 +64,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
// The navigation stack doesn't like it if the root and the push happen // The navigation stack doesn't like it if the root and the push happen
// on the same loop run // on the same loop run
DispatchQueue.main.async { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
self.presentSecureBackupScreen(animated: animated) self.presentSecureBackupScreen(animated: animated)
} }
default: default:
@@ -115,7 +115,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case .dismissedSettings: case .dismissedSettings:
stateMachine.processEvent(.dismissedSettingsScreen) stateMachine.processEvent(.dismissedSettingsScreen)
case .runLogoutFlow: case .runLogoutFlow:
runLogoutFlow() Task { await self.runLogoutFlow() }
case .clearCache: case .clearCache:
actionsSubject.send(.clearCache) actionsSubject.send(.clearCache)
case .forceLogout: case .forceLogout:
@@ -330,10 +330,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.processEvent(.feedbackScreen) stateMachine.processEvent(.feedbackScreen)
case .presentSessionVerificationScreen: case .presentSessionVerificationScreen:
stateMachine.processEvent(.showSessionVerificationScreen) stateMachine.processEvent(.showSessionVerificationScreen)
case .presentSecureBackupSettings:
settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
case .presentStartChatScreen: case .presentStartChatScreen:
stateMachine.processEvent(.showStartChatScreen) stateMachine.processEvent(.showStartChatScreen)
case .logout: case .logout:
runLogoutFlow() Task { await self.runLogoutFlow() }
case .presentInvitesScreen: case .presentInvitesScreen:
stateMachine.processEvent(.showInvitesScreen) stateMachine.processEvent(.showInvitesScreen)
} }
@@ -358,10 +360,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func runLogoutFlow() { private func runLogoutFlow() async {
let secureBackupController = userSession.clientProxy.secureBackupController let secureBackupController = userSession.clientProxy.secureBackupController
guard secureBackupController.isLastSession, appSettings.chatBackupEnabled else { guard case let .success(isLastSession) = await secureBackupController.isLastSession() else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init())
return
}
guard isLastSession, appSettings.chatBackupEnabled else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(), ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutRecoveryDisabledTitle, title: L10n.screenSignoutRecoveryDisabledTitle,
message: L10n.screenSignoutRecoveryDisabledSubtitle, message: L10n.screenSignoutRecoveryDisabledSubtitle,
+4 -4
View File
@@ -380,6 +380,10 @@ public enum L10n {
public static var commonWaiting: String { return L10n.tr("Localizable", "common_waiting") } public static var commonWaiting: String { return L10n.tr("Localizable", "common_waiting") }
/// Waiting for decryption key /// Waiting for decryption key
public static var commonWaitingForDecryptionKey: String { return L10n.tr("Localizable", "common_waiting_for_decryption_key") } public static var commonWaitingForDecryptionKey: String { return L10n.tr("Localizable", "common_waiting_for_decryption_key") }
/// Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup.
public static var confirmRecoveryKeyBannerMessage: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_message") }
/// Confirm your recovery key
public static var confirmRecoveryKeyBannerTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_title") }
/// %1$@ crashed the last time it was used. Would you like to share a crash report with us? /// %1$@ crashed the last time it was used. Would you like to share a crash report with us?
public static func crashDetectionDialogContent(_ p1: Any) -> String { public static func crashDetectionDialogContent(_ p1: Any) -> String {
return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1)) return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1))
@@ -442,10 +446,6 @@ public enum L10n {
} }
/// Failed to upload your voice message. /// Failed to upload your voice message.
public static var errorFailedUploadingVoiceMessage: String { return L10n.tr("Localizable", "error_failed_uploading_voice_message") } public static var errorFailedUploadingVoiceMessage: String { return L10n.tr("Localizable", "error_failed_uploading_voice_message") }
/// %1$@ does not have permission to access your location. You can enable access in Settings > Location
public static func errorMissingLocationAuthIos(_ p1: Any) -> String {
return L10n.tr("Localizable", "error_missing_location_auth_ios", String(describing: p1))
}
/// No compatible app was found to handle this action. /// No compatible app was found to handle this action.
public static var errorNoCompatibleAppFound: String { return L10n.tr("Localizable", "error_no_compatible_app_found") } public static var errorNoCompatibleAppFound: String { return L10n.tr("Localizable", "error_no_compatible_app_found") }
/// Some messages have not been sent /// Some messages have not been sent
@@ -2607,44 +2607,39 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol {
set(value) { underlyingKeyBackupState = value } set(value) { underlyingKeyBackupState = value }
} }
var underlyingKeyBackupState: CurrentValuePublisher<SecureBackupKeyBackupState, Never>! var underlyingKeyBackupState: CurrentValuePublisher<SecureBackupKeyBackupState, Never>!
var isLastSession: Bool {
get { return underlyingIsLastSession } //MARK: - enable
set(value) { underlyingIsLastSession = value }
var enableCallsCount = 0
var enableCalled: Bool {
return enableCallsCount > 0
} }
var underlyingIsLastSession: Bool! var enableReturnValue: Result<Void, SecureBackupControllerError>!
var enableClosure: (() async -> Result<Void, SecureBackupControllerError>)?
//MARK: - enableBackup func enable() async -> Result<Void, SecureBackupControllerError> {
enableCallsCount += 1
var enableBackupCallsCount = 0 if let enableClosure = enableClosure {
var enableBackupCalled: Bool { return await enableClosure()
return enableBackupCallsCount > 0
}
var enableBackupReturnValue: Result<Void, SecureBackupControllerError>!
var enableBackupClosure: (() async -> Result<Void, SecureBackupControllerError>)?
func enableBackup() async -> Result<Void, SecureBackupControllerError> {
enableBackupCallsCount += 1
if let enableBackupClosure = enableBackupClosure {
return await enableBackupClosure()
} else { } else {
return enableBackupReturnValue return enableReturnValue
} }
} }
//MARK: - disableBackup //MARK: - disable
var disableBackupCallsCount = 0 var disableCallsCount = 0
var disableBackupCalled: Bool { var disableCalled: Bool {
return disableBackupCallsCount > 0 return disableCallsCount > 0
} }
var disableBackupReturnValue: Result<Void, SecureBackupControllerError>! var disableReturnValue: Result<Void, SecureBackupControllerError>!
var disableBackupClosure: (() async -> Result<Void, SecureBackupControllerError>)? var disableClosure: (() async -> Result<Void, SecureBackupControllerError>)?
func disableBackup() async -> Result<Void, SecureBackupControllerError> { func disable() async -> Result<Void, SecureBackupControllerError> {
disableBackupCallsCount += 1 disableCallsCount += 1
if let disableBackupClosure = disableBackupClosure { if let disableClosure = disableClosure {
return await disableBackupClosure() return await disableClosure()
} else { } else {
return disableBackupReturnValue return disableReturnValue
} }
} }
//MARK: - generateRecoveryKey //MARK: - generateRecoveryKey
@@ -2685,6 +2680,23 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol {
return confirmRecoveryKeyReturnValue return confirmRecoveryKeyReturnValue
} }
} }
//MARK: - isLastSession
var isLastSessionCallsCount = 0
var isLastSessionCalled: Bool {
return isLastSessionCallsCount > 0
}
var isLastSessionReturnValue: Result<Bool, SecureBackupControllerError>!
var isLastSessionClosure: (() async -> Result<Bool, SecureBackupControllerError>)?
func isLastSession() async -> Result<Bool, SecureBackupControllerError> {
isLastSessionCallsCount += 1
if let isLastSessionClosure = isLastSessionClosure {
return await isLastSessionClosure()
} else {
return isLastSessionReturnValue
}
}
//MARK: - waitForKeyBackup //MARK: - waitForKeyBackup
var waitForKeyBackupCallsCount = 0 var waitForKeyBackupCallsCount = 0
@@ -68,6 +68,7 @@ struct A11yIdentifiers {
let settings = "home_screen-settings" let settings = "home_screen-settings"
let signOut = "home_screen-sign_out" let signOut = "home_screen-sign_out"
let verificationBannerContinue = "home_screen-verification_continue" let verificationBannerContinue = "home_screen-verification_continue"
let recoveryKeyConfirmationBannerContinue = "home_screen-recovery_key_confirmation_continue"
let invites = "home_screen-invites" let invites = "home_screen-invites"
let startChat = "home_screen-start_chat" let startChat = "home_screen-start_chat"
@@ -32,6 +32,7 @@ enum HomeScreenCoordinatorAction {
case presentSettingsScreen case presentSettingsScreen
case presentFeedbackScreen case presentFeedbackScreen
case presentSessionVerificationScreen case presentSessionVerificationScreen
case presentSecureBackupSettings
case presentStartChatScreen case presentStartChatScreen
case presentInvitesScreen case presentInvitesScreen
case logout case logout
@@ -75,6 +76,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentSettingsScreen) actionsSubject.send(.presentSettingsScreen)
case .presentSessionVerificationScreen: case .presentSessionVerificationScreen:
actionsSubject.send(.presentSessionVerificationScreen) actionsSubject.send(.presentSessionVerificationScreen)
case .presentSecureBackupSettings:
actionsSubject.send(.presentSecureBackupSettings)
case .logout: case .logout:
actionsSubject.send(.logout) actionsSubject.send(.logout)
case .presentStartChatScreen: case .presentStartChatScreen:
@@ -23,6 +23,7 @@ enum HomeScreenViewModelAction {
case presentRoomDetails(roomIdentifier: String) case presentRoomDetails(roomIdentifier: String)
case roomLeft(roomIdentifier: String) case roomLeft(roomIdentifier: String)
case presentSessionVerificationScreen case presentSessionVerificationScreen
case presentSecureBackupSettings
case presentSettingsScreen case presentSettingsScreen
case presentFeedbackScreen case presentFeedbackScreen
case presentStartChatScreen case presentStartChatScreen
@@ -44,7 +45,9 @@ enum HomeScreenViewAction {
case userMenu(action: HomeScreenViewUserMenuAction) case userMenu(action: HomeScreenViewUserMenuAction)
case startChat case startChat
case verifySession case verifySession
case confirmRecoveryKey
case skipSessionVerification case skipSessionVerification
case skipRecoveryKeyConfirmation
case updateVisibleItemRange(range: Range<Int>, isScrolling: Bool) case updateVisibleItemRange(range: Range<Int>, isScrolling: Bool)
case selectInvites case selectInvites
} }
@@ -70,7 +73,8 @@ struct HomeScreenViewState: BindableState {
let userID: String let userID: String
var userDisplayName: String? var userDisplayName: String?
var userAvatarURL: URL? var userAvatarURL: URL?
var showSessionVerificationBanner = false var needsSessionVerification = false
var needsRecoveryKeyConfirmation = false
var showUserMenuBadge = false var showUserMenuBadge = false
var showSettingsMenuOptionBadge = false var showSettingsMenuOptionBadge = false
var rooms: [HomeScreenRoom] = [] var rooms: [HomeScreenRoom] = []
@@ -61,9 +61,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
.sink { [weak self] callback in .sink { [weak self] callback in
switch callback { switch callback {
case .sessionVerificationNeeded: case .sessionVerificationNeeded:
self?.state.showSessionVerificationBanner = true self?.state.needsSessionVerification = true
case .didVerifySession: case .didVerifySession:
self?.state.showSessionVerificationBanner = false self?.state.needsSessionVerification = false
default: default:
break break
} }
@@ -82,14 +82,15 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
userSession.clientProxy.secureBackupController.recoveryKeyState userSession.clientProxy.secureBackupController.recoveryKeyState
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.map { state in .sink { [weak self] recoveryKeyState in
state == .unknown || state == .disabled guard let self, appSettings.chatBackupEnabled else { return }
}
.sink { [weak self] requiresSecureBackupSetup in
guard let self else { return }
state.showUserMenuBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled let requiresSecureBackupSetup = recoveryKeyState == .unknown || recoveryKeyState == .disabled || recoveryKeyState == .incomplete
state.showSettingsMenuOptionBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled
state.showUserMenuBadge = requiresSecureBackupSetup
state.showSettingsMenuOptionBadge = requiresSecureBackupSetup
state.needsRecoveryKeyConfirmation = recoveryKeyState == .incomplete
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -143,8 +144,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
} }
case .verifySession: case .verifySession:
actionsSubject.send(.presentSessionVerificationScreen) actionsSubject.send(.presentSessionVerificationScreen)
case .confirmRecoveryKey:
actionsSubject.send(.presentSecureBackupSettings)
case .skipSessionVerification: case .skipSessionVerification:
state.showSessionVerificationBanner = false state.needsSessionVerification = false
case .skipRecoveryKeyConfirmation:
state.needsRecoveryKeyConfirmation = false
case .updateVisibleItemRange(let range, let isScrolling): case .updateVisibleItemRange(let range, let isScrolling):
visibleItemRangePublisher.send((range, isScrolling)) visibleItemRangePublisher.send((range, isScrolling))
case .startChat: case .startChat:
@@ -191,8 +191,10 @@ struct HomeScreen: View {
@ViewBuilder @ViewBuilder
/// The session verification banner and invites button if either are needed. /// The session verification banner and invites button if either are needed.
private var topSection: some View { private var topSection: some View {
if context.viewState.showSessionVerificationBanner { if context.viewState.needsSessionVerification {
sessionVerificationBanner HomeScreenSessionVerificationBanner(context: context)
} else if context.viewState.needsRecoveryKeyConfirmation {
HomeScreenRecoveryKeyConfirmationBanner(context: context)
} }
if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused { if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused {
@@ -204,43 +206,7 @@ struct HomeScreen: View {
.padding(.vertical, -8.0) .padding(.vertical, -8.0)
} }
} }
private var sessionVerificationBanner: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 16) {
Text(L10n.sessionVerificationBannerTitle)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
Spacer()
Button {
context.send(viewAction: .skipSessionVerification)
} label: {
Image(systemName: "xmark")
.foregroundColor(.compound.iconSecondary)
.frame(width: 12, height: 12)
}
}
Text(L10n.sessionVerificationBannerMessage)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
}
Button(L10n.actionContinue) {
context.send(viewAction: .verifySession)
}
.frame(maxWidth: .infinity)
.buttonStyle(.compound(.primary, size: .medium))
.accessibilityIdentifier(A11yIdentifiers.homeScreen.verificationBannerContinue)
}
.padding(16)
.background(Color.compound.bgSubtleSecondary)
.cornerRadius(14)
.padding(.horizontal, 16)
}
@ToolbarContentBuilder @ToolbarContentBuilder
private var toolbar: some ToolbarContent { private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
@@ -333,13 +299,18 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview {
} }
static func viewModel(_ state: MockRoomSummaryProviderState) -> HomeScreenViewModel { static func viewModel(_ state: MockRoomSummaryProviderState) -> HomeScreenViewModel {
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com", let clientProxy = MockClientProxy(userID: "@alice:example.com",
roomSummaryProvider: MockRoomSummaryProvider(state: state)), roomSummaryProvider: MockRoomSummaryProvider(state: state))
let userSession = MockUserSession(clientProxy: clientProxy,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock()) voiceMessageMediaManager: VoiceMessageMediaManagerMock())
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())
return HomeScreenViewModel(userSession: userSession, return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder()), attributedStringBuilder: attributedStringBuilder,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics, analytics: ServiceLocator.shared.analytics,
@@ -0,0 +1,85 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
struct HomeScreenRecoveryKeyConfirmationBanner: View {
@ObservedObject var context: HomeScreenViewModel.Context
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 16) {
Text(L10n.confirmRecoveryKeyBannerTitle)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
Spacer()
Button {
context.send(viewAction: .skipRecoveryKeyConfirmation)
} label: {
Image(systemName: "xmark")
.foregroundColor(.compound.iconSecondary)
.frame(width: 12, height: 12)
}
}
Text(L10n.confirmRecoveryKeyBannerMessage)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
}
Button(L10n.actionContinue) {
context.send(viewAction: .confirmRecoveryKey)
}
.frame(maxWidth: .infinity)
.buttonStyle(.compound(.primary, size: .medium))
.accessibilityIdentifier(A11yIdentifiers.homeScreen.recoveryKeyConfirmationBannerContinue)
}
.padding(16)
.background(Color.compound.bgSubtleSecondary)
.cornerRadius(14)
.padding(.horizontal, 16)
}
}
struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, TestablePreview {
static let viewModel = buildViewModel()
static var previews: some View {
HomeScreenRecoveryKeyConfirmationBanner(context: viewModel.context)
}
static func buildViewModel() -> HomeScreenViewModel {
let clientProxy = MockClientProxy(userID: "@alice:example.com",
roomSummaryProvider: MockRoomSummaryProvider(state: .loading))
let userSession = MockUserSession(clientProxy: clientProxy,
mediaProvider: MockMediaProvider(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())
return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: attributedStringBuilder,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}
@@ -0,0 +1,85 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
struct HomeScreenSessionVerificationBanner: View {
@ObservedObject var context: HomeScreenViewModel.Context
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 16) {
Text(L10n.sessionVerificationBannerTitle)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
Spacer()
Button {
context.send(viewAction: .skipSessionVerification)
} label: {
Image(systemName: "xmark")
.foregroundColor(.compound.iconSecondary)
.frame(width: 12, height: 12)
}
}
Text(L10n.sessionVerificationBannerMessage)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
}
Button(L10n.actionContinue) {
context.send(viewAction: .verifySession)
}
.frame(maxWidth: .infinity)
.buttonStyle(.compound(.primary, size: .medium))
.accessibilityIdentifier(A11yIdentifiers.homeScreen.verificationBannerContinue)
}
.padding(16)
.background(Color.compound.bgSubtleSecondary)
.cornerRadius(14)
.padding(.horizontal, 16)
}
}
struct HomeScreenSessionVerificationBanner_Previews: PreviewProvider, TestablePreview {
static let viewModel = buildViewModel()
static var previews: some View {
HomeScreenSessionVerificationBanner(context: viewModel.context)
}
static func buildViewModel() -> HomeScreenViewModel {
let clientProxy = MockClientProxy(userID: "@alice:example.com",
roomSummaryProvider: MockRoomSummaryProvider(state: .loading))
let userSession = MockUserSession(clientProxy: clientProxy,
mediaProvider: MockMediaProvider(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())
return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: attributedStringBuilder,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}
@@ -70,22 +70,10 @@ class SecureBackupKeyBackupScreenViewModel: SecureBackupKeyBackupScreenViewModel
} }
// MARK: - Private // MARK: - Private
private func enableBackup() {
Task {
switch await secureBackupController.enableBackup() {
case .success:
actionsSubject.send(.done)
case .failure(let error):
MXLog.error("Failed enabling key backup with error: \(error)")
state.bindings.alertInfo = .init(id: .init())
}
}
}
private func disableBackup() { private func disableBackup() {
Task { Task {
switch await secureBackupController.disableBackup() { switch await secureBackupController.disable() {
case .success: case .success:
actionsSubject.send(.done) actionsSubject.send(.done)
case .failure(let error): case .failure(let error):
@@ -55,8 +55,8 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
actionsSubject.send(.recoveryKey) actionsSubject.send(.recoveryKey)
case .keyBackup: case .keyBackup:
switch secureBackupController.keyBackupState.value { switch secureBackupController.keyBackupState.value {
case .disabled: case .disabled, .unknown:
enableKeyBackup() enableBackup()
case .enabled: case .enabled:
actionsSubject.send(.keyBackup) actionsSubject.send(.keyBackup)
default: default:
@@ -67,11 +67,11 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
// MARK: - Private // MARK: - Private
private func enableKeyBackup() { private func enableBackup() {
Task { Task {
let loadingIndicatorIdentifier = "SecureBackupScreenLoading" let loadingIndicatorIdentifier = "SecureBackupScreenLoading"
userIndicatorController?.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) userIndicatorController?.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
switch await secureBackupController.enableBackup() { switch await secureBackupController.enable() {
case .success: case .success:
break break
case .failure(let error): case .failure(let error):
@@ -77,12 +77,10 @@ struct SecureBackupScreen: View {
ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionDisable, role: .destructive), kind: .navigationLink { ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionDisable, role: .destructive), kind: .navigationLink {
context.send(viewAction: .keyBackup) context.send(viewAction: .keyBackup)
}) })
case .disabled, .enabling: case .disabled, .enabling, .unknown:
ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionEnable), kind: .navigationLink { ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionEnable), kind: .navigationLink {
context.send(viewAction: .keyBackup) context.send(viewAction: .keyBackup)
}) })
default:
ListRow(label: .plain(title: L10n.commonLoading), details: .isWaiting(true), kind: .label)
} }
} }
@@ -96,7 +96,7 @@ class ClientProxy: ClientProxyProtocol {
notificationSettings = NotificationSettingsProxy(notificationSettings: client.getNotificationSettings(), notificationSettings = NotificationSettingsProxy(notificationSettings: client.getNotificationSettings(),
backgroundTaskService: backgroundTaskService) backgroundTaskService: backgroundTaskService)
secureBackupController = SecureBackupController() secureBackupController = SecureBackupController(encryption: client.encryption())
client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
self?.hasEncounteredAuthError = true self?.hasEncounteredAuthError = true
@@ -44,6 +44,7 @@ class MockClientProxy: ClientProxyProtocol {
let secureBackupController = SecureBackupControllerMock() let secureBackupController = SecureBackupControllerMock()
secureBackupController.underlyingRecoveryKeyState = .init(CurrentValueSubject<SecureBackupRecoveryKeyState, Never>(.disabled)) secureBackupController.underlyingRecoveryKeyState = .init(CurrentValueSubject<SecureBackupRecoveryKeyState, Never>(.disabled))
secureBackupController.underlyingKeyBackupState = .init(CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled)) secureBackupController.underlyingKeyBackupState = .init(CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled))
secureBackupController.isLastSessionReturnValue = .success(false)
return secureBackupController return secureBackupController
}() }()
@@ -16,10 +16,16 @@
import Combine import Combine
import Foundation import Foundation
import MatrixRustSDK
class SecureBackupController: SecureBackupControllerProtocol { class SecureBackupController: SecureBackupControllerProtocol {
private let recoveryKeyStateSubject = CurrentValueSubject<SecureBackupRecoveryKeyState, Never>(.disabled) private let encryption: Encryption
private let keyBackupStateSubject = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled)
private let recoveryKeyStateSubject = CurrentValueSubject<SecureBackupRecoveryKeyState, Never>(.unknown)
private let keyBackupStateSubject = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.unknown)
private var backupStateListenerTaskHandle: TaskHandle?
private var recoveryStateListenerTaskHandle: TaskHandle?
var recoveryKeyState: CurrentValuePublisher<SecureBackupRecoveryKeyState, Never> { var recoveryKeyState: CurrentValuePublisher<SecureBackupRecoveryKeyState, Never> {
recoveryKeyStateSubject.asCurrentValuePublisher() recoveryKeyStateSubject.asCurrentValuePublisher()
@@ -29,60 +35,153 @@ class SecureBackupController: SecureBackupControllerProtocol {
keyBackupStateSubject.asCurrentValuePublisher() keyBackupStateSubject.asCurrentValuePublisher()
} }
var isLastSession: Bool { init(encryption: Encryption) {
#warning("FIXME") self.encryption = encryption
return true
backupStateListenerTaskHandle = encryption.backupStateListener(listener: SecureBackupControllerBackupStateListener { [weak self] state in
self?.keyBackupStateSubject.send(state.keyBackupState)
})
recoveryStateListenerTaskHandle = encryption.recoveryStateListener(listener: SecureBackupRecoveryStateListener { [weak self] state in
guard let self else { return }
switch state {
case .unknown:
recoveryKeyStateSubject.send(.unknown)
case .enabled:
recoveryKeyStateSubject.send(.enabled)
case .disabled:
recoveryKeyStateSubject.send(.disabled)
case .incomplete:
recoveryKeyStateSubject.send(.incomplete)
}
})
} }
func enableBackup() async -> Result<Void, SecureBackupControllerError> { func enable() async -> Result<Void, SecureBackupControllerError> {
keyBackupStateSubject.send(.enabling) do {
try await encryption.enableBackups()
try? await Task.sleep(for: .seconds(1)) } catch {
return .failure(.failedEnablingBackup)
keyBackupStateSubject.send(.enabled)
return .success(())
}
func disableBackup() async -> Result<Void, SecureBackupControllerError> {
keyBackupStateSubject.send(.disabling)
try? await Task.sleep(for: .seconds(1))
keyBackupStateSubject.send(.disabled)
return .success(())
}
static var wohoo = 0
func generateRecoveryKey() async -> Result<String, SecureBackupControllerError> {
recoveryKeyStateSubject.send(.settingUp)
try? await Task.sleep(for: .seconds(1))
if Self.wohoo > 0, Self.wohoo % 2 == 1 {
recoveryKeyStateSubject.send(.incomplete)
} else {
recoveryKeyStateSubject.send(.enabled)
} }
Self.wohoo += 1 return .success(())
}
func disable() async -> Result<Void, SecureBackupControllerError> {
do {
try await encryption.disableRecovery()
} catch {
return .failure(.failedDisablingBackup)
}
return .success(UUID().uuidString) return .success(())
}
func generateRecoveryKey() async -> Result<String, SecureBackupControllerError> {
do {
guard recoveryKeyState.value == .disabled else {
let key = try await encryption.resetRecoveryKey()
return .success(key)
}
let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, progressListener: SecureBackupEnableRecoveryProgressListener { [weak self] state in
guard let self else { return }
switch state {
case .creatingBackup:
recoveryKeyStateSubject.send(.settingUp)
case .creatingRecoveryKey:
recoveryKeyStateSubject.send(.settingUp)
case .backingUp:
recoveryKeyStateSubject.send(.settingUp)
case .done:
recoveryKeyStateSubject.send(.enabled)
}
})
return .success(recoveryKey)
} catch {
return .failure(.failedGeneratingRecoveryKey)
}
} }
func confirmRecoveryKey(_ key: String) async -> Result<Void, SecureBackupControllerError> { func confirmRecoveryKey(_ key: String) async -> Result<Void, SecureBackupControllerError> {
recoveryKeyStateSubject.send(.settingUp) do {
try await encryption.fixRecoveryIssues(recoveryKey: key)
try? await Task.sleep(for: .seconds(1)) return .success(())
} catch {
recoveryKeyStateSubject.send(.enabled) return .failure(.failedConfirmingRecoveryKey)
}
return .success(()) }
func isLastSession() async -> Result<Bool, SecureBackupControllerError> {
do {
return try await .success(encryption.isLastDevice())
} catch {
return .failure(.failedFetchingSessionState)
}
} }
func waitForKeyBackup() async { func waitForKeyBackup() async {
try? await Task.sleep(for: .seconds(5)) await encryption.waitForBackupUploadSteadyState(progressListener: nil)
}
}
private final class SecureBackupControllerBackupStateListener: BackupStateListener {
private let onUpdateClosure: (BackupState) -> Void
init(_ onUpdateClosure: @escaping (BackupState) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func onUpdate(status: BackupState) {
onUpdateClosure(status)
}
}
private final class SecureBackupRecoveryStateListener: RecoveryStateListener {
private let onUpdateClosure: (RecoveryState) -> Void
init(_ onUpdateClosure: @escaping (RecoveryState) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func onUpdate(status: RecoveryState) {
onUpdateClosure(status)
}
}
private final class SecureBackupEnableRecoveryProgressListener: EnableRecoveryProgressListener {
private let onUpdateClosure: (EnableRecoveryProgress) -> Void
init(_ onUpdateClosure: @escaping (EnableRecoveryProgress) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func onUpdate(status: EnableRecoveryProgress) {
onUpdateClosure(status)
}
}
extension BackupState {
var keyBackupState: SecureBackupKeyBackupState {
switch self {
case .unknown:
return .unknown
case .creating:
return .enabling
case .enabling:
return .enabling
case .resuming:
return .enabled
case .enabled:
return .enabled
case .downloading:
return .enabled
case .disabling:
return .disabling
case .disabled:
return .disabled
}
} }
} }
@@ -28,6 +28,8 @@ enum SecureBackupRecoveryKeyState {
} }
enum SecureBackupKeyBackupState { enum SecureBackupKeyBackupState {
/// Any state where backups couldn't have been enabled but we didn't explicitly disable them on this client.
/// For all intents and purposes, within the client, this can be treated as `disabled`.
case unknown case unknown
case enabling case enabling
case enabled case enabled
@@ -36,11 +38,13 @@ enum SecureBackupKeyBackupState {
} }
enum SecureBackupControllerError: Error { enum SecureBackupControllerError: Error {
case failedEnablingKeyBackup case failedEnablingBackup
case failedDisablingKeyBackup case failedDisablingBackup
case failedGeneratingRecoveryKey case failedGeneratingRecoveryKey
case failedConfirmingRecoveryKey case failedConfirmingRecoveryKey
case failedFetchingSessionState
} }
// sourcery: AutoMockable // sourcery: AutoMockable
@@ -49,14 +53,14 @@ protocol SecureBackupControllerProtocol {
var keyBackupState: CurrentValuePublisher<SecureBackupKeyBackupState, Never> { get } var keyBackupState: CurrentValuePublisher<SecureBackupKeyBackupState, Never> { get }
var isLastSession: Bool { get } func enable() async -> Result<Void, SecureBackupControllerError>
func disable() async -> Result<Void, SecureBackupControllerError>
func enableBackup() async -> Result<Void, SecureBackupControllerError>
func disableBackup() async -> Result<Void, SecureBackupControllerError>
func generateRecoveryKey() async -> Result<String, SecureBackupControllerError> func generateRecoveryKey() async -> Result<String, SecureBackupControllerError>
func confirmRecoveryKey(_ key: String) async -> Result<Void, SecureBackupControllerError> func confirmRecoveryKey(_ key: String) async -> Result<Void, SecureBackupControllerError>
func isLastSession() async -> Result<Bool, SecureBackupControllerError>
func waitForKeyBackup() async func waitForKeyBackup() async
} }
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fdf6448bf514d93817c55d1fe66f55e62ff102767aa5654ac2a6c0dee80d79e2
size 99786
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f5362fc231ca0c55b95136d8c6953877df8aeaa9151be49e8eb95596f05e237
size 95997