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:
Doug 2024-10-28 12:28:13 +00:00 committed by GitHub
parent 4e812f72b9
commit 7c28d9709f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 175 additions and 116 deletions

View File

@ -27,6 +27,7 @@ struct SecureBackupRecoveryKeyScreenViewState: BindableState {
let mode: SecureBackupRecoveryKeyScreenViewMode
var recoveryKey: String?
var isGeneratingKey = false
var doneButtonEnabled = false
var bindings: SecureBackupRecoveryKeyScreenViewBindings

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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),

View File

@ -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) })