From a837ce5cff7eced95fb555790012e1ef66b7e315 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 27 Oct 2023 22:16:51 +0300 Subject: [PATCH] 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 --- ElementX.xcodeproj/project.pbxproj | 8 + .../en.lproj/Localizable.strings | 3 +- .../SettingsFlowCoordinator.swift | 2 +- .../UserSessionFlowCoordinator.swift | 15 +- ElementX/Sources/Generated/Strings.swift | 8 +- .../Mocks/Generated/GeneratedMocks.swift | 70 ++++--- .../Other/AccessibilityIdentifiers.swift | 1 + .../HomeScreen/HomeScreenCoordinator.swift | 3 + .../Screens/HomeScreen/HomeScreenModels.swift | 6 +- .../HomeScreen/HomeScreenViewModel.swift | 25 ++- .../Screens/HomeScreen/View/HomeScreen.swift | 55 ++--- ...eScreenRecoveryKeyConfirmationBanner.swift | 85 ++++++++ .../HomeScreenSessionVerificationBanner.swift | 85 ++++++++ ...SecureBackupKeyBackupScreenViewModel.swift | 16 +- .../SecureBackupScreenViewModel.swift | 8 +- .../View/SecureBackupScreen.swift | 4 +- .../Sources/Services/Client/ClientProxy.swift | 2 +- .../Services/Client/MockClientProxy.swift | 1 + .../SecureBackup/SecureBackupController.swift | 191 +++++++++++++----- .../SecureBackupControllerProtocol.swift | 16 +- ...eScreenRecoveryKeyConfirmationBanner.1.png | 3 + ..._homeScreenSessionVerificationBanner.1.png | 3 + 22 files changed, 444 insertions(+), 166 deletions(-) create mode 100644 ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift create mode 100644 ElementX/Sources/Screens/HomeScreen/View/HomeScreenSessionVerificationBanner.swift create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_homeScreenRecoveryKeyConfirmationBanner.1.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_homeScreenSessionVerificationBanner.1.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 27b2b012f..959ee96a7 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -319,6 +319,7 @@ 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 5770C4906668C6D3008A2AC9 /* SessionVerificationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.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 */; }; 5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.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 */; }; AFA1F2543DFF7B45DF68ACD6 /* CompletionSuggestionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170BF6F7923A5C3792442F27 /* CompletionSuggestionModels.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 */; }; B09DC6E3D0EE87C4D4ABFAB3 /* EncryptedHistoryRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */; }; 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 = ""; }; 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = ""; }; 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = ""; }; + 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = ""; }; 05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = ""; }; 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = ""; }; 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1832,6 +1835,7 @@ E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = ""; }; E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxy.swift; sourceTree = ""; }; E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactory.swift; sourceTree = ""; }; + E71C28CF29CD05B6D6AE8580 /* HomeScreenSessionVerificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenSessionVerificationBanner.swift; sourceTree = ""; }; E80F9E9B93B6ECE9A937B1C6 /* FormRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormRow.swift; sourceTree = ""; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = ""; }; @@ -2865,8 +2869,10 @@ B902EA6CD3296B0E10EE432B /* HomeScreen.swift */, C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */, 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */, + 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */, ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */, C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */, + E71C28CF29CD05B6D6AE8580 /* HomeScreenSessionVerificationBanner.swift */, F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */, ); path = View; @@ -5447,8 +5453,10 @@ 77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */, 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */, 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */, + B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */, 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */, A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */, + 584590D0EA548152A393E72C /* HomeScreenSessionVerificationBanner.swift in Sources */, D3986615892E7CF05C86518A /* HomeScreenUserMenuButton.swift in Sources */, DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */, 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 6761d1403..1f3593e80 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -172,6 +172,8 @@ "common_waiting_for_decryption_key" = "Waiting for decryption key"; "common_poll_end_confirmation" = "Are you sure you want to end this poll?"; "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?"; "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."; @@ -196,7 +198,6 @@ "error_failed_loading_messages" = "Failed loading messages"; "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_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_some_messages_have_not_been_sent" = "Some messages have not been sent"; "error_unknown" = "Sorry, an error occurred"; diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 67c9ebc4d..87f98fcdd 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -64,7 +64,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { // The navigation stack doesn't like it if the root and the push happen // on the same loop run - DispatchQueue.main.async { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { self.presentSecureBackupScreen(animated: animated) } default: diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index db1d9e5e9..2d268106b 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -115,7 +115,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case .dismissedSettings: stateMachine.processEvent(.dismissedSettingsScreen) case .runLogoutFlow: - runLogoutFlow() + Task { await self.runLogoutFlow() } case .clearCache: actionsSubject.send(.clearCache) case .forceLogout: @@ -330,10 +330,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { stateMachine.processEvent(.feedbackScreen) case .presentSessionVerificationScreen: stateMachine.processEvent(.showSessionVerificationScreen) + case .presentSecureBackupSettings: + settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true) case .presentStartChatScreen: stateMachine.processEvent(.showStartChatScreen) case .logout: - runLogoutFlow() + Task { await self.runLogoutFlow() } case .presentInvitesScreen: stateMachine.processEvent(.showInvitesScreen) } @@ -358,10 +360,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } } - private func runLogoutFlow() { + private func runLogoutFlow() async { 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(), title: L10n.screenSignoutRecoveryDisabledTitle, message: L10n.screenSignoutRecoveryDisabledSubtitle, diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 0d0036c48..990957fc4 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -380,6 +380,10 @@ public enum L10n { public static var commonWaiting: String { return L10n.tr("Localizable", "common_waiting") } /// 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? public static func crashDetectionDialogContent(_ p1: Any) -> String { return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1)) @@ -442,10 +446,6 @@ public enum L10n { } /// Failed to upload your 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. public static var errorNoCompatibleAppFound: String { return L10n.tr("Localizable", "error_no_compatible_app_found") } /// Some messages have not been sent diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index e796cf3f5..91166478f 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2607,44 +2607,39 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { set(value) { underlyingKeyBackupState = value } } var underlyingKeyBackupState: CurrentValuePublisher! - var isLastSession: Bool { - get { return underlyingIsLastSession } - set(value) { underlyingIsLastSession = value } + + //MARK: - enable + + var enableCallsCount = 0 + var enableCalled: Bool { + return enableCallsCount > 0 } - var underlyingIsLastSession: Bool! + var enableReturnValue: Result! + var enableClosure: (() async -> Result)? - //MARK: - enableBackup - - var enableBackupCallsCount = 0 - var enableBackupCalled: Bool { - return enableBackupCallsCount > 0 - } - var enableBackupReturnValue: Result! - var enableBackupClosure: (() async -> Result)? - - func enableBackup() async -> Result { - enableBackupCallsCount += 1 - if let enableBackupClosure = enableBackupClosure { - return await enableBackupClosure() + func enable() async -> Result { + enableCallsCount += 1 + if let enableClosure = enableClosure { + return await enableClosure() } else { - return enableBackupReturnValue + return enableReturnValue } } - //MARK: - disableBackup + //MARK: - disable - var disableBackupCallsCount = 0 - var disableBackupCalled: Bool { - return disableBackupCallsCount > 0 + var disableCallsCount = 0 + var disableCalled: Bool { + return disableCallsCount > 0 } - var disableBackupReturnValue: Result! - var disableBackupClosure: (() async -> Result)? + var disableReturnValue: Result! + var disableClosure: (() async -> Result)? - func disableBackup() async -> Result { - disableBackupCallsCount += 1 - if let disableBackupClosure = disableBackupClosure { - return await disableBackupClosure() + func disable() async -> Result { + disableCallsCount += 1 + if let disableClosure = disableClosure { + return await disableClosure() } else { - return disableBackupReturnValue + return disableReturnValue } } //MARK: - generateRecoveryKey @@ -2685,6 +2680,23 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { return confirmRecoveryKeyReturnValue } } + //MARK: - isLastSession + + var isLastSessionCallsCount = 0 + var isLastSessionCalled: Bool { + return isLastSessionCallsCount > 0 + } + var isLastSessionReturnValue: Result! + var isLastSessionClosure: (() async -> Result)? + + func isLastSession() async -> Result { + isLastSessionCallsCount += 1 + if let isLastSessionClosure = isLastSessionClosure { + return await isLastSessionClosure() + } else { + return isLastSessionReturnValue + } + } //MARK: - waitForKeyBackup var waitForKeyBackupCallsCount = 0 diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 41a0f64ea..f7d74cc3d 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -68,6 +68,7 @@ struct A11yIdentifiers { let settings = "home_screen-settings" let signOut = "home_screen-sign_out" let verificationBannerContinue = "home_screen-verification_continue" + let recoveryKeyConfirmationBannerContinue = "home_screen-recovery_key_confirmation_continue" let invites = "home_screen-invites" let startChat = "home_screen-start_chat" diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index c7bbd9e1d..6f6c3e761 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -32,6 +32,7 @@ enum HomeScreenCoordinatorAction { case presentSettingsScreen case presentFeedbackScreen case presentSessionVerificationScreen + case presentSecureBackupSettings case presentStartChatScreen case presentInvitesScreen case logout @@ -75,6 +76,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentSettingsScreen) case .presentSessionVerificationScreen: actionsSubject.send(.presentSessionVerificationScreen) + case .presentSecureBackupSettings: + actionsSubject.send(.presentSecureBackupSettings) case .logout: actionsSubject.send(.logout) case .presentStartChatScreen: diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 9990d3dbe..58a2be908 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -23,6 +23,7 @@ enum HomeScreenViewModelAction { case presentRoomDetails(roomIdentifier: String) case roomLeft(roomIdentifier: String) case presentSessionVerificationScreen + case presentSecureBackupSettings case presentSettingsScreen case presentFeedbackScreen case presentStartChatScreen @@ -44,7 +45,9 @@ enum HomeScreenViewAction { case userMenu(action: HomeScreenViewUserMenuAction) case startChat case verifySession + case confirmRecoveryKey case skipSessionVerification + case skipRecoveryKeyConfirmation case updateVisibleItemRange(range: Range, isScrolling: Bool) case selectInvites } @@ -70,7 +73,8 @@ struct HomeScreenViewState: BindableState { let userID: String var userDisplayName: String? var userAvatarURL: URL? - var showSessionVerificationBanner = false + var needsSessionVerification = false + var needsRecoveryKeyConfirmation = false var showUserMenuBadge = false var showSettingsMenuOptionBadge = false var rooms: [HomeScreenRoom] = [] diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 688dec9de..6007bdff9 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -61,9 +61,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .sink { [weak self] callback in switch callback { case .sessionVerificationNeeded: - self?.state.showSessionVerificationBanner = true + self?.state.needsSessionVerification = true case .didVerifySession: - self?.state.showSessionVerificationBanner = false + self?.state.needsSessionVerification = false default: break } @@ -82,14 +82,15 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol userSession.clientProxy.secureBackupController.recoveryKeyState .receive(on: DispatchQueue.main) - .map { state in - state == .unknown || state == .disabled - } - .sink { [weak self] requiresSecureBackupSetup in - guard let self else { return } + .sink { [weak self] recoveryKeyState in + guard let self, appSettings.chatBackupEnabled else { return } - state.showUserMenuBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled - state.showSettingsMenuOptionBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled + let requiresSecureBackupSetup = recoveryKeyState == .unknown || recoveryKeyState == .disabled || recoveryKeyState == .incomplete + + state.showUserMenuBadge = requiresSecureBackupSetup + state.showSettingsMenuOptionBadge = requiresSecureBackupSetup + + state.needsRecoveryKeyConfirmation = recoveryKeyState == .incomplete } .store(in: &cancellables) @@ -143,8 +144,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } case .verifySession: actionsSubject.send(.presentSessionVerificationScreen) + case .confirmRecoveryKey: + actionsSubject.send(.presentSecureBackupSettings) case .skipSessionVerification: - state.showSessionVerificationBanner = false + state.needsSessionVerification = false + case .skipRecoveryKeyConfirmation: + state.needsRecoveryKeyConfirmation = false case .updateVisibleItemRange(let range, let isScrolling): visibleItemRangePublisher.send((range, isScrolling)) case .startChat: diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 26afd89a3..f5f5e6236 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -191,8 +191,10 @@ struct HomeScreen: View { @ViewBuilder /// The session verification banner and invites button if either are needed. private var topSection: some View { - if context.viewState.showSessionVerificationBanner { - sessionVerificationBanner + if context.viewState.needsSessionVerification { + HomeScreenSessionVerificationBanner(context: context) + } else if context.viewState.needsRecoveryKeyConfirmation { + HomeScreenRecoveryKeyConfirmationBanner(context: context) } if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused { @@ -204,43 +206,7 @@ struct HomeScreen: View { .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 private var toolbar: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { @@ -333,13 +299,18 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview { } static func viewModel(_ state: MockRoomSummaryProviderState) -> HomeScreenViewModel { - let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com", - roomSummaryProvider: MockRoomSummaryProvider(state: state)), + let clientProxy = MockClientProxy(userID: "@alice:example.com", + roomSummaryProvider: MockRoomSummaryProvider(state: state)) + + 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(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder()), + attributedStringBuilder: attributedStringBuilder, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift new file mode 100644 index 000000000..68abd4ba8 --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift @@ -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(nil).asCurrentValuePublisher(), + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) + } +} diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenSessionVerificationBanner.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenSessionVerificationBanner.swift new file mode 100644 index 000000000..f7b0bdb0d --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenSessionVerificationBanner.swift @@ -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(nil).asCurrentValuePublisher(), + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift index 793cfd200..ba6a8b2da 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift @@ -70,22 +70,10 @@ class SecureBackupKeyBackupScreenViewModel: SecureBackupKeyBackupScreenViewModel } // 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() { Task { - switch await secureBackupController.disableBackup() { + switch await secureBackupController.disable() { case .success: actionsSubject.send(.done) case .failure(let error): diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift index aacba1732..d9b4e9827 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift @@ -55,8 +55,8 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup actionsSubject.send(.recoveryKey) case .keyBackup: switch secureBackupController.keyBackupState.value { - case .disabled: - enableKeyBackup() + case .disabled, .unknown: + enableBackup() case .enabled: actionsSubject.send(.keyBackup) default: @@ -67,11 +67,11 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup // MARK: - Private - private func enableKeyBackup() { + private func enableBackup() { Task { let loadingIndicatorIdentifier = "SecureBackupScreenLoading" userIndicatorController?.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) - switch await secureBackupController.enableBackup() { + switch await secureBackupController.enable() { case .success: break case .failure(let error): diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift index 2e6990b6a..c48333fa9 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift @@ -77,12 +77,10 @@ struct SecureBackupScreen: View { ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionDisable, role: .destructive), kind: .navigationLink { context.send(viewAction: .keyBackup) }) - case .disabled, .enabling: + case .disabled, .enabling, .unknown: ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionEnable), kind: .navigationLink { context.send(viewAction: .keyBackup) }) - default: - ListRow(label: .plain(title: L10n.commonLoading), details: .isWaiting(true), kind: .label) } } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 63a36f148..6eca06902 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -96,7 +96,7 @@ class ClientProxy: ClientProxyProtocol { notificationSettings = NotificationSettingsProxy(notificationSettings: client.getNotificationSettings(), backgroundTaskService: backgroundTaskService) - secureBackupController = SecureBackupController() + secureBackupController = SecureBackupController(encryption: client.encryption()) client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in self?.hasEncounteredAuthError = true diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index 52686cdf6..6354ee514 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -44,6 +44,7 @@ class MockClientProxy: ClientProxyProtocol { let secureBackupController = SecureBackupControllerMock() secureBackupController.underlyingRecoveryKeyState = .init(CurrentValueSubject(.disabled)) secureBackupController.underlyingKeyBackupState = .init(CurrentValueSubject(.enabled)) + secureBackupController.isLastSessionReturnValue = .success(false) return secureBackupController }() diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift index be0b49b9a..2c9389ccb 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift @@ -16,10 +16,16 @@ import Combine import Foundation +import MatrixRustSDK class SecureBackupController: SecureBackupControllerProtocol { - private let recoveryKeyStateSubject = CurrentValueSubject(.disabled) - private let keyBackupStateSubject = CurrentValueSubject(.enabled) + private let encryption: Encryption + + private let recoveryKeyStateSubject = CurrentValueSubject(.unknown) + private let keyBackupStateSubject = CurrentValueSubject(.unknown) + + private var backupStateListenerTaskHandle: TaskHandle? + private var recoveryStateListenerTaskHandle: TaskHandle? var recoveryKeyState: CurrentValuePublisher { recoveryKeyStateSubject.asCurrentValuePublisher() @@ -29,60 +35,153 @@ class SecureBackupController: SecureBackupControllerProtocol { keyBackupStateSubject.asCurrentValuePublisher() } - var isLastSession: Bool { - #warning("FIXME") - return true + init(encryption: Encryption) { + self.encryption = encryption + + 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 { - keyBackupStateSubject.send(.enabling) - - try? await Task.sleep(for: .seconds(1)) - - keyBackupStateSubject.send(.enabled) - - return .success(()) - } - - func disableBackup() async -> Result { - keyBackupStateSubject.send(.disabling) - - try? await Task.sleep(for: .seconds(1)) - - keyBackupStateSubject.send(.disabled) - - return .success(()) - } - - static var wohoo = 0 - - func generateRecoveryKey() async -> Result { - 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) + func enable() async -> Result { + do { + try await encryption.enableBackups() + } catch { + return .failure(.failedEnablingBackup) } - Self.wohoo += 1 + return .success(()) + } + + func disable() async -> Result { + do { + try await encryption.disableRecovery() + } catch { + return .failure(.failedDisablingBackup) + } - return .success(UUID().uuidString) + return .success(()) + } + + func generateRecoveryKey() async -> Result { + 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 { - recoveryKeyStateSubject.send(.settingUp) - - try? await Task.sleep(for: .seconds(1)) - - recoveryKeyStateSubject.send(.enabled) - - return .success(()) + do { + try await encryption.fixRecoveryIssues(recoveryKey: key) + return .success(()) + } catch { + return .failure(.failedConfirmingRecoveryKey) + } + } + + func isLastSession() async -> Result { + do { + return try await .success(encryption.isLastDevice()) + } catch { + return .failure(.failedFetchingSessionState) + } } 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 + } } } diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift index ba04c326a..fff182a2f 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift @@ -28,6 +28,8 @@ enum SecureBackupRecoveryKeyState { } 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 enabling case enabled @@ -36,11 +38,13 @@ enum SecureBackupKeyBackupState { } enum SecureBackupControllerError: Error { - case failedEnablingKeyBackup - case failedDisablingKeyBackup + case failedEnablingBackup + case failedDisablingBackup case failedGeneratingRecoveryKey case failedConfirmingRecoveryKey + + case failedFetchingSessionState } // sourcery: AutoMockable @@ -49,14 +53,14 @@ protocol SecureBackupControllerProtocol { var keyBackupState: CurrentValuePublisher { get } - var isLastSession: Bool { get } - - func enableBackup() async -> Result - func disableBackup() async -> Result + func enable() async -> Result + func disable() async -> Result func generateRecoveryKey() async -> Result func confirmRecoveryKey(_ key: String) async -> Result + func isLastSession() async -> Result + func waitForKeyBackup() async } diff --git a/UnitTests/__Snapshots__/PreviewTests/test_homeScreenRecoveryKeyConfirmationBanner.1.png b/UnitTests/__Snapshots__/PreviewTests/test_homeScreenRecoveryKeyConfirmationBanner.1.png new file mode 100644 index 000000000..aad9cc267 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_homeScreenRecoveryKeyConfirmationBanner.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdf6448bf514d93817c55d1fe66f55e62ff102767aa5654ac2a6c0dee80d79e2 +size 99786 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_homeScreenSessionVerificationBanner.1.png b/UnitTests/__Snapshots__/PreviewTests/test_homeScreenSessionVerificationBanner.1.png new file mode 100644 index 000000000..738592363 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_homeScreenSessionVerificationBanner.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f5362fc231ca0c55b95136d8c6953877df8aeaa9151be49e8eb95596f05e237 +size 95997