mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
Tweak the flow for changing a recovery key. (#3452)
* Rename Chat Backup setting to Encryption. * Update Key Storage strings on SecureBackupScreen. * Update strings/design on SecureBackupRecoveryKeyScreen.
This commit is contained in:
parent
4e812f72b9
commit
7c28d9709f
@ -27,6 +27,7 @@ struct SecureBackupRecoveryKeyScreenViewState: BindableState {
|
||||
let mode: SecureBackupRecoveryKeyScreenViewMode
|
||||
|
||||
var recoveryKey: String?
|
||||
var isGeneratingKey = false
|
||||
var doneButtonEnabled = false
|
||||
|
||||
var bindings: SecureBackupRecoveryKeyScreenViewBindings
|
||||
|
@ -49,6 +49,8 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
|
||||
switch viewAction {
|
||||
case .generateKey:
|
||||
state.isGeneratingKey = true
|
||||
|
||||
Task {
|
||||
switch await secureBackupController.generateRecoveryKey() {
|
||||
case .success(let key):
|
||||
@ -58,7 +60,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
state.bindings.alertInfo = .init(id: .init())
|
||||
}
|
||||
|
||||
hideLoadingIndicator()
|
||||
state.isGeneratingKey = false
|
||||
}
|
||||
case .copyKey:
|
||||
UIPasteboard.general.string = state.recoveryKey
|
||||
|
@ -18,7 +18,6 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
FullscreenDialog {
|
||||
ScrollViewReader { reader in
|
||||
mainContent
|
||||
.padding(16)
|
||||
.onChange(of: focused) { _, newValue in
|
||||
guard newValue == true else { return }
|
||||
reader.scrollTo(textFieldIdentifier)
|
||||
@ -94,12 +93,12 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
}
|
||||
|
||||
private var recoveryCreatedActionButtons: some View {
|
||||
VStack(spacing: 8.0) {
|
||||
VStack(spacing: 16) {
|
||||
if let recoveryKey = context.viewState.recoveryKey {
|
||||
ShareLink(item: recoveryKey) {
|
||||
Label(L10n.screenRecoveryKeySaveAction, icon: \.download)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.buttonStyle(.compound(.secondary))
|
||||
.simultaneousGesture(TapGesture().onEnded { _ in
|
||||
context.send(viewAction: .keySaved)
|
||||
})
|
||||
@ -131,20 +130,31 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
Text(L10n.commonRecoveryKey)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.padding(.leading, 16)
|
||||
|
||||
Group {
|
||||
if context.viewState.recoveryKey == nil {
|
||||
Button(generateButtonTitle) {
|
||||
context.send(viewAction: .generateKey)
|
||||
if !context.viewState.isGeneratingKey {
|
||||
Button(generateButtonTitle) {
|
||||
context.send(viewAction: .generateKey)
|
||||
}
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.padding(.vertical, 11)
|
||||
} else {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView()
|
||||
Text(L10n.screenRecoveryKeyGeneratingKey)
|
||||
}
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.padding(.vertical, 11)
|
||||
}
|
||||
.font(.compound.bodyLGSemibold)
|
||||
} else {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Text(context.viewState.recoveryKey ?? "")
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.bodyLG)
|
||||
|
||||
Spacer()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Button {
|
||||
context.send(viewAction: .copyKey)
|
||||
@ -157,21 +167,16 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.padding(.vertical, 14)
|
||||
.padding(.horizontal, 16)
|
||||
.background(Color.compound.bgSubtleSecondaryLevel0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
|
||||
if let subtitle = context.viewState.recoveryKeySubtitle {
|
||||
Label {
|
||||
Text(subtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodySM)
|
||||
} icon: {
|
||||
if context.viewState.recoveryKey == nil {
|
||||
CompoundIcon(\.infoSolid, size: .small, relativeTo: .compound.bodySM)
|
||||
}
|
||||
}
|
||||
.labelStyle(.custom(spacing: 8, alignment: .top))
|
||||
Text(subtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodySM)
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -212,8 +217,10 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let setupViewModel = viewModel(recoveryState: .enabled)
|
||||
static let key = "EsTM njec uHYA yHmh dQdW Nj4o bNRU 9jMN XGMc KUNM UFr5 R8GY"
|
||||
static let notSetUpViewModel = viewModel(recoveryState: .disabled)
|
||||
static let generatingViewModel = viewModel(recoveryState: .disabled, generateKey: true)
|
||||
static let setupViewModel = viewModel(recoveryState: .enabled, generateKey: true, key: key)
|
||||
static let incompleteViewModel = viewModel(recoveryState: .incomplete)
|
||||
static let unknownViewModel = viewModel(recoveryState: .unknown)
|
||||
|
||||
@ -223,10 +230,17 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview
|
||||
}
|
||||
.previewDisplayName("Not set up")
|
||||
|
||||
NavigationStack {
|
||||
SecureBackupRecoveryKeyScreen(context: generatingViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Generating")
|
||||
.snapshot(delay: 0.25)
|
||||
|
||||
NavigationStack {
|
||||
SecureBackupRecoveryKeyScreen(context: setupViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Set up")
|
||||
.snapshot(delay: 0.25)
|
||||
|
||||
NavigationStack {
|
||||
SecureBackupRecoveryKeyScreen(context: incompleteViewModel.context)
|
||||
@ -239,12 +253,27 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview
|
||||
.previewDisplayName("Unknown")
|
||||
}
|
||||
|
||||
static func viewModel(recoveryState: SecureBackupRecoveryState) -> SecureBackupRecoveryKeyScreenViewModelType {
|
||||
static func viewModel(recoveryState: SecureBackupRecoveryState, generateKey: Bool = false, key: String? = nil) -> SecureBackupRecoveryKeyScreenViewModelType {
|
||||
let backupController = SecureBackupControllerMock()
|
||||
backupController.underlyingRecoveryState = CurrentValueSubject<SecureBackupRecoveryState, Never>(recoveryState).asCurrentValuePublisher()
|
||||
|
||||
return SecureBackupRecoveryKeyScreenViewModel(secureBackupController: backupController,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
isModallyPresented: true)
|
||||
if let key {
|
||||
backupController.generateRecoveryKeyReturnValue = .success(key)
|
||||
} else {
|
||||
backupController.generateRecoveryKeyClosure = {
|
||||
try? await Task.sleep(for: .seconds(1000))
|
||||
return .success("youshouldntseeme")
|
||||
}
|
||||
}
|
||||
|
||||
let viewModel = SecureBackupRecoveryKeyScreenViewModel(secureBackupController: backupController,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
isModallyPresented: true)
|
||||
|
||||
if generateKey {
|
||||
viewModel.context.send(viewAction: .generateKey)
|
||||
}
|
||||
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
@ -16,14 +16,15 @@ struct SecureBackupScreenViewState: BindableState {
|
||||
let chatBackupDetailsURL: URL
|
||||
var recoveryState = SecureBackupRecoveryState.unknown
|
||||
var keyBackupState = SecureBackupKeyBackupState.unknown
|
||||
var bindings = SecureBackupScreenViewStateBindings()
|
||||
var bindings: SecureBackupScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct SecureBackupScreenViewStateBindings {
|
||||
var keyStorageEnabled: Bool
|
||||
var alertInfo: AlertInfo<UUID>?
|
||||
}
|
||||
|
||||
enum SecureBackupScreenViewAction {
|
||||
case recoveryKey
|
||||
case keyBackup
|
||||
case keyStorageToggled(Bool)
|
||||
}
|
||||
|
@ -25,7 +25,8 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
|
||||
self.secureBackupController = secureBackupController
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: .init(chatBackupDetailsURL: chatBackupDetailsURL))
|
||||
super.init(initialViewState: .init(chatBackupDetailsURL: chatBackupDetailsURL,
|
||||
bindings: SecureBackupScreenViewStateBindings(keyStorageEnabled: secureBackupController.keyBackupState.value.toggleState)))
|
||||
|
||||
secureBackupController.recoveryState
|
||||
.receive(on: DispatchQueue.main)
|
||||
@ -34,7 +35,11 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
|
||||
|
||||
secureBackupController.keyBackupState
|
||||
.receive(on: DispatchQueue.main)
|
||||
.weakAssign(to: \.state.keyBackupState, on: self)
|
||||
.sink { [weak self] state in
|
||||
guard let self else { return }
|
||||
self.state.keyBackupState = state
|
||||
self.state.bindings.keyStorageEnabled = state.toggleState
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@ -44,11 +49,14 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
|
||||
switch viewAction {
|
||||
case .recoveryKey:
|
||||
actionsSubject.send(.recoveryKey)
|
||||
case .keyBackup:
|
||||
switch secureBackupController.keyBackupState.value {
|
||||
case .unknown:
|
||||
case .keyStorageToggled(let enable):
|
||||
let keyBackupState = secureBackupController.keyBackupState.value
|
||||
switch (keyBackupState, enable) {
|
||||
case (.unknown, true):
|
||||
state.bindings.keyStorageEnabled = keyBackupState.toggleState // Reset the toggle in case enabling fails
|
||||
enableBackup()
|
||||
case .enabled:
|
||||
case (.enabled, false):
|
||||
state.bindings.keyStorageEnabled = keyBackupState.toggleState // Reset the toggle in case the user cancels
|
||||
actionsSubject.send(.keyBackup)
|
||||
default:
|
||||
break
|
||||
@ -74,3 +82,12 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SecureBackupKeyBackupState {
|
||||
var toggleState: Bool {
|
||||
switch self {
|
||||
case .unknown, .enabling: false
|
||||
case .enabled, .disabling: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ struct SecureBackupScreen: View {
|
||||
private var keyBackupSection: some View {
|
||||
Section {
|
||||
ListRow(kind: .custom {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(L10n.screenChatBackupKeyBackupTitle)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
@ -53,7 +53,7 @@ struct SecureBackupScreen: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
})
|
||||
|
||||
keyBackupButton
|
||||
keyStorageToggle
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,26 +67,23 @@ struct SecureBackupScreen: View {
|
||||
return description
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var keyBackupButton: some View {
|
||||
switch context.viewState.keyBackupState {
|
||||
case .enabled, .disabling:
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionDisable, role: .destructive), kind: .navigationLink {
|
||||
context.send(viewAction: .keyBackup)
|
||||
})
|
||||
case .unknown, .enabling:
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionEnable), kind: .navigationLink {
|
||||
context.send(viewAction: .keyBackup)
|
||||
})
|
||||
}
|
||||
private var keyStorageToggle: some View {
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupKeyStorageToggleTitle,
|
||||
description: L10n.screenChatBackupKeyStorageToggleDescription),
|
||||
kind: .toggle($context.keyStorageEnabled))
|
||||
.onChange(of: context.keyStorageEnabled) { _, newValue in
|
||||
context.send(viewAction: .keyStorageToggled(newValue))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var recoveryKeySection: some View {
|
||||
Section {
|
||||
switch context.viewState.recoveryState {
|
||||
case .enabled:
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionChange),
|
||||
ListRow(label: .default(title: L10n.screenChatBackupRecoveryActionChange,
|
||||
description: L10n.screenChatBackupRecoveryActionChangeDescription,
|
||||
icon: \.key,
|
||||
iconAlignment: .top),
|
||||
kind: .navigationLink { context.send(viewAction: .recoveryKey) })
|
||||
case .disabled:
|
||||
ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionSetup),
|
||||
|
@ -90,7 +90,7 @@ struct SettingsScreen: View {
|
||||
|
||||
switch context.viewState.securitySectionMode {
|
||||
case .secureBackup:
|
||||
ListRow(label: .default(title: L10n.commonChatBackup,
|
||||
ListRow(label: .default(title: L10n.commonEncryption,
|
||||
icon: \.key),
|
||||
details: context.viewState.showSecuritySectionBadge ? .icon(securitySectionBadge) : nil,
|
||||
kind: .navigationLink { context.send(viewAction: .secureBackup) })
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Generating.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Generating.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Set-up.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Set-up.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-pseudo.Generating.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-pseudo.Generating.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-en-GB.Generating.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-en-GB.Generating.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-pseudo.Generating.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-pseudo.Generating.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPad-en-GB.Both-setup.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPad-en-GB.Both-setup.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPad-pseudo.Both-setup.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPad-pseudo.Both-setup.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPhone-16-en-GB.Both-setup.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPhone-16-en-GB.Both-setup.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPhone-16-pseudo.Both-setup.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupScreen-iPhone-16-pseudo.Both-setup.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user