Reset Recovery Key Screen (#2648)

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Mauro
2024-04-04 18:14:22 +02:00
committed by GitHub
parent 97d9189077
commit 023b7fc608
32 changed files with 458 additions and 37 deletions
@@ -156,7 +156,7 @@
"common_privacy_policy" = "Privacy policy";
"common_reaction" = "Reaction";
"common_reactions" = "Reactions";
"common_recovery_key" = "Recovery key";
"common_recovery_key" = "Recovery key or passcode";
"common_refreshing" = "Refreshing…";
"common_replying_to" = "Replying to %1$@";
"common_report_a_bug" = "Report a bug";
@@ -374,6 +374,13 @@
"screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync.";
"screen_chat_backup_recovery_action_setup" = "Set up recovery";
"screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere.";
"screen_create_new_recovery_key_list_item_1" = "Open Element in a desktop device";
"screen_create_new_recovery_key_list_item_2" = "Sign into your account again";
"screen_create_new_recovery_key_list_item_3" = "When asked to verify your device, select %1$@";
"screen_create_new_recovery_key_list_item_3_reset_all" = "“Reset all”";
"screen_create_new_recovery_key_list_item_4" = "Follow the instructions to create a new recovery key";
"screen_create_new_recovery_key_list_item_5" = "Save your new recovery key in a password manager or encrypted note";
"screen_create_new_recovery_key_title" = "Reset the encryption for your account using another device";
"screen_create_poll_add_option_btn" = "Add option";
"screen_create_poll_anonymous_desc" = "Show results only after poll ends";
"screen_create_poll_anonymous_headline" = "Hide votes";
@@ -398,6 +405,7 @@
"screen_edit_profile_error_title" = "Unable to update profile";
"screen_edit_profile_title" = "Edit profile";
"screen_edit_profile_updating_details" = "Updating profile…";
"screen_identity_confirmation_create_new_recovery_key" = "Create a new recovery key";
"screen_identity_confirmation_subtitle" = "Verify this device to set up secure messaging.";
"screen_identity_confirmation_title" = "Confirm that it's you";
"screen_identity_confirmed_subtitle" = "Now you can read or send messages securely, and anyone you chat with can also trust this device.";
@@ -473,13 +481,14 @@
"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe";
"screen_recovery_key_change_success" = "Recovery key changed";
"screen_recovery_key_change_title" = "Change recovery key?";
"screen_recovery_key_confirm_description" = "Enter your recovery key to confirm access to your chat backup.";
"screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key";
"screen_recovery_key_confirm_description" = "Make sure nobody can see this screen!";
"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup.";
"screen_recovery_key_confirm_error_title" = "Incorrect recovery key";
"screen_recovery_key_confirm_key_description" = "Enter the 48 character code.";
"screen_recovery_key_confirm_key_description" = "If you have a recovery passphrase or secret passphrase/key, this will work too.";
"screen_recovery_key_confirm_key_placeholder" = "Enter…";
"screen_recovery_key_confirm_success" = "Recovery key confirmed";
"screen_recovery_key_confirm_title" = "Enter your recovery key";
"screen_recovery_key_confirm_title" = "Enter your recovery key or passcode";
"screen_recovery_key_copied_to_clipboard" = "Copied recovery key";
"screen_recovery_key_generating_key" = "Generating…";
"screen_recovery_key_save_action" = "Save recovery key";
@@ -258,6 +258,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
case .recoveryFixed:
appSettings.hasRunIdentityConfirmationOnboarding = true
stateMachine.tryEvent(.next)
case .showResetKeyInfo:
presentResetRecoveryKeyScreen()
default:
fatalError("Other flows shouldn't be possible")
}
@@ -267,6 +269,18 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
presentCoordinator(coordinator)
}
private func presentResetRecoveryKeyScreen() {
let coordinator = ResetRecoveryKeyScreenCoordinator()
coordinator.actionsPublisher.sink { [weak self] action in
switch action {
case .cancel:
self?.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setSheetCoordinator(coordinator)
}
private func presentIdentityConfirmedScreen() {
let coordinator = IdentityConfirmedScreenCoordinator(parameters: .init())
coordinator.actionsPublisher
+24 -4
View File
@@ -364,7 +364,7 @@ internal enum L10n {
internal static var commonReaction: String { return L10n.tr("Localizable", "common_reaction") }
/// Reactions
internal static var commonReactions: String { return L10n.tr("Localizable", "common_reactions") }
/// Recovery key
/// Recovery key or passcode
internal static var commonRecoveryKey: String { return L10n.tr("Localizable", "common_recovery_key") }
/// Refreshing…
internal static var commonRefreshing: String { return L10n.tr("Localizable", "common_refreshing") }
@@ -919,6 +919,22 @@ internal enum L10n {
internal static func screenChatBackupRecoveryActionSetupDescription(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_chat_backup_recovery_action_setup_description", String(describing: p1))
}
/// Open Element in a desktop device
internal static var screenCreateNewRecoveryKeyListItem1: String { return L10n.tr("Localizable", "screen_create_new_recovery_key_list_item_1") }
/// Sign into your account again
internal static var screenCreateNewRecoveryKeyListItem2: String { return L10n.tr("Localizable", "screen_create_new_recovery_key_list_item_2") }
/// When asked to verify your device, select %1$@
internal static func screenCreateNewRecoveryKeyListItem3(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_create_new_recovery_key_list_item_3", String(describing: p1))
}
/// “Reset all”
internal static var screenCreateNewRecoveryKeyListItem3ResetAll: String { return L10n.tr("Localizable", "screen_create_new_recovery_key_list_item_3_reset_all") }
/// Follow the instructions to create a new recovery key
internal static var screenCreateNewRecoveryKeyListItem4: String { return L10n.tr("Localizable", "screen_create_new_recovery_key_list_item_4") }
/// Save your new recovery key in a password manager or encrypted note
internal static var screenCreateNewRecoveryKeyListItem5: String { return L10n.tr("Localizable", "screen_create_new_recovery_key_list_item_5") }
/// Reset the encryption for your account using another device
internal static var screenCreateNewRecoveryKeyTitle: String { return L10n.tr("Localizable", "screen_create_new_recovery_key_title") }
/// Add option
internal static var screenCreatePollAddOptionBtn: String { return L10n.tr("Localizable", "screen_create_poll_add_option_btn") }
/// Show results only after poll ends
@@ -991,6 +1007,8 @@ internal enum L10n {
internal static var screenEditProfileTitle: String { return L10n.tr("Localizable", "screen_edit_profile_title") }
/// Updating profile…
internal static var screenEditProfileUpdatingDetails: String { return L10n.tr("Localizable", "screen_edit_profile_updating_details") }
/// Create a new recovery key
internal static var screenIdentityConfirmationCreateNewRecoveryKey: String { return L10n.tr("Localizable", "screen_identity_confirmation_create_new_recovery_key") }
/// Verify this device to set up secure messaging.
internal static var screenIdentityConfirmationSubtitle: String { return L10n.tr("Localizable", "screen_identity_confirmation_subtitle") }
/// Confirm that it's you
@@ -1159,19 +1177,21 @@ internal enum L10n {
internal static var screenRecoveryKeyChangeSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_change_success") }
/// Change recovery key?
internal static var screenRecoveryKeyChangeTitle: String { return L10n.tr("Localizable", "screen_recovery_key_change_title") }
/// Enter your recovery key to confirm access to your chat backup.
/// Create new recovery key
internal static var screenRecoveryKeyConfirmCreateNewRecoveryKey: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_create_new_recovery_key") }
/// Make sure nobody can see this screen!
internal static var screenRecoveryKeyConfirmDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_description") }
/// Please try again to confirm access to your chat backup.
internal static var screenRecoveryKeyConfirmErrorContent: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_content") }
/// Incorrect recovery key
internal static var screenRecoveryKeyConfirmErrorTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_title") }
/// Enter the 48 character code.
/// If you have a recovery passphrase or secret passphrase/key, this will work too.
internal static var screenRecoveryKeyConfirmKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_description") }
/// Enter…
internal static var screenRecoveryKeyConfirmKeyPlaceholder: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_placeholder") }
/// Recovery key confirmed
internal static var screenRecoveryKeyConfirmSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_success") }
/// Enter your recovery key
/// Enter your recovery key or passcode
internal static var screenRecoveryKeyConfirmTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_title") }
/// Copied recovery key
internal static var screenRecoveryKeyCopiedToClipboard: String { return L10n.tr("Localizable", "screen_recovery_key_copied_to_clipboard") }
@@ -24,6 +24,7 @@ struct HeroImage: View {
enum Style {
case normal
case positive
case subtle
var foregroundColor: Color {
switch self {
@@ -31,6 +32,8 @@ struct HeroImage: View {
return .compound.iconPrimary
case .positive:
return .compound.iconSuccessPrimary
case .subtle:
return .compound.iconSecondary
}
}
@@ -40,6 +43,8 @@ struct HeroImage: View {
return .compound.bgSubtleSecondary
case .positive:
return .compound.bgSuccessSubtle
case .subtle:
return .compound.bgSubtlePrimary
}
}
}
@@ -0,0 +1,56 @@
//
// Copyright 2022 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.
//
// periphery:ignore:all - this is just a resetKey remove this comment once generating the final file
import Combine
import SwiftUI
enum ResetRecoveryKeyScreenCoordinatorAction {
case cancel
}
final class ResetRecoveryKeyScreenCoordinator: CoordinatorProtocol {
private let viewModel: ResetRecoveryKeyScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<ResetRecoveryKeyScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<ResetRecoveryKeyScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
viewModel = ResetRecoveryKeyScreenViewModel()
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .cancel:
self.actionsSubject.send(.cancel)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(ResetRecoveryKeyScreen(context: viewModel.context))
}
}
@@ -0,0 +1,36 @@
//
// Copyright 2022 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 Foundation
enum ResetRecoveryKeyScreenViewModelAction {
case cancel
}
struct ResetRecoveryKeyScreenViewState: BindableState {
let listItem3AttributedText = {
let boldPlaceholder = "{bold}"
var finalString = AttributedString(L10n.screenCreateNewRecoveryKeyListItem3(boldPlaceholder))
var boldString = AttributedString(L10n.screenCreateNewRecoveryKeyListItem3ResetAll)
boldString.bold()
finalString.replace(boldPlaceholder, with: boldString)
return finalString
}()
}
enum ResetRecoveryKeyScreenViewAction {
case cancel
}
@@ -0,0 +1,40 @@
//
// Copyright 2022 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
typealias ResetRecoveryKeyScreenViewModelType = StateStoreViewModel<ResetRecoveryKeyScreenViewState, ResetRecoveryKeyScreenViewAction>
class ResetRecoveryKeyScreenViewModel: ResetRecoveryKeyScreenViewModelType, ResetRecoveryKeyScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<ResetRecoveryKeyScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<ResetRecoveryKeyScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: ResetRecoveryKeyScreenViewState())
}
// MARK: - Public
override func process(viewAction: ResetRecoveryKeyScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.cancel)
}
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 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
@MainActor
protocol ResetRecoveryKeyScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<ResetRecoveryKeyScreenViewModelAction, Never> { get }
var context: ResetRecoveryKeyScreenViewModelType.Context { get }
}
@@ -0,0 +1,134 @@
//
// Copyright 2022 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 Compound
import SwiftUI
struct ResetRecoveryKeyScreen: View {
@ObservedObject var context: ResetRecoveryKeyScreenViewModel.Context
var body: some View {
NavigationStack {
FullscreenDialog {
mainContent
} bottomContent: {
EmptyView()
}
.toolbar { toolbar }
.toolbar(.visible, for: .navigationBar)
.background()
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgSubtleSecondary))
.interactiveDismissDisabled()
}
}
private var mainContent: some View {
VStack(spacing: 40) {
header
list
}
}
private var header: some View {
VStack(spacing: 16) {
HeroImage(icon: \.computer, style: .subtle)
Text(L10n.screenCreateNewRecoveryKeyTitle)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
}
}
private var list: some View {
VStack(alignment: .leading, spacing: 24) {
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem1)
} icon: {
Image(systemSymbol: ._1Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem2)
} icon: {
Image(systemSymbol: ._2Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(context.viewState.listItem3AttributedText)
} icon: {
Image(systemSymbol: ._3Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem4)
} icon: {
Image(systemSymbol: ._4Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem5)
} icon: {
Image(systemSymbol: ._5Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
}
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
}
// MARK: - Previews
struct ResetRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = ResetRecoveryKeyScreenViewModel()
static var previews: some View {
NavigationStack {
ResetRecoveryKeyScreen(context: viewModel.context)
}
}
}
@@ -28,6 +28,7 @@ enum SecureBackupRecoveryKeyScreenCoordinatorAction {
case recoverySetUp
case recoveryChanged
case recoveryFixed
case showResetKeyInfo
}
final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
@@ -62,6 +63,8 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
case .fixRecovery:
self.actionsSubject.send(.recoveryFixed)
}
case .showResetKeyInfo:
self.actionsSubject.send(.showResetKeyInfo)
}
}
.store(in: &cancellables)
@@ -19,6 +19,7 @@ import Foundation
enum SecureBackupRecoveryKeyScreenViewModelAction {
case done(mode: SecureBackupRecoveryKeyScreenViewMode)
case cancel
case showResetKeyInfo
}
enum SecureBackupRecoveryKeyScreenViewMode {
@@ -82,6 +83,7 @@ enum SecureBackupRecoveryKeyScreenViewAction {
case copyKey
case keySaved
case confirmKey
case resetKey
case done
case cancel
}
@@ -100,6 +100,8 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
guard let self else { return }
actionsSubject.send(.done(mode: context.viewState.mode))
}))
case .resetKey:
actionsSubject.send(.showResetKeyInfo)
}
}
}
@@ -64,6 +64,12 @@ struct SecureBackupRecoveryKeyScreen: View {
case .setupRecovery, .changeRecovery:
recoveryCreatedActionButtons
case .fixRecovery:
incompleteVerificationActionButtons
}
}
private var incompleteVerificationActionButtons: some View {
VStack(spacing: 16) {
Button {
context.send(viewAction: .confirmKey)
} label: {
@@ -71,6 +77,14 @@ struct SecureBackupRecoveryKeyScreen: View {
}
.buttonStyle(.compound(.primary))
.disabled(context.confirmationRecoveryKey.isEmpty)
Button {
context.send(viewAction: .resetKey)
} label: {
Text(L10n.screenIdentityConfirmationCreateNewRecoveryKey)
.padding(.vertical, 14)
}
.buttonStyle(.compound(.plain))
}
}
@@ -127,7 +141,7 @@ struct SecureBackupRecoveryKeyScreen: View {
VStack(alignment: .leading, spacing: 8) {
Text(L10n.commonRecoveryKey)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodySM)
.font(.compound.bodySMSemibold)
Group {
if context.viewState.recoveryKey == nil {
@@ -180,7 +194,7 @@ struct SecureBackupRecoveryKeyScreen: View {
VStack(alignment: .leading, spacing: 8) {
Text(L10n.commonRecoveryKey)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodySM)
.font(.compound.bodySMSemibold)
SecureField(L10n.screenRecoveryKeyConfirmKeyPlaceholder, text: $context.confirmationRecoveryKey)
.textContentType(.password) // Not ideal but stops random suggestions
@@ -51,18 +51,20 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol {
recoveryKeyCoordinator.actions.sink { [weak self] action in
guard let self else { return }
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
switch action {
case .cancel:
break
case .recoverySetUp:
showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess)
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
case .recoveryChanged:
showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess)
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
case .recoveryFixed:
showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess)
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
case .showResetKeyInfo:
showResetRecoveryKeyScreen(navigationStackCoordinator: navigationStackCoordinator)
}
}
.store(in: &cancellables)
@@ -105,4 +107,16 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol {
iconName: "checkmark",
persistent: false))
}
private func showResetRecoveryKeyScreen(navigationStackCoordinator: NavigationStackCoordinator) {
let coordinator = ResetRecoveryKeyScreenCoordinator()
coordinator.actionsPublisher.sink { action in
switch action {
case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setSheetCoordinator(coordinator)
}
}