Make sure we display errors when we create a recovery key and it fails (#5079)

* Make sure we display errors when we create a recovery key and it fails

* Add another preview for the error state

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2025-07-25 13:36:43 +02:00
committed by GitHub
parent 7958bb4692
commit 2750835a36
11 changed files with 86 additions and 11 deletions

View File

@@ -60,6 +60,7 @@ class SecureBackupSetupPresenter @AssistedInject constructor(
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey)
SecureBackupSetupEvents.DismissDialog -> {
showSaveConfirmationDialog = false
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.ClearError)
}
SecureBackupSetupEvents.Done -> {
showSaveConfirmationDialog = true
@@ -89,6 +90,7 @@ class SecureBackupSetupPresenter @AssistedInject constructor(
SecureBackupSetupStateMachine.State.CreatingKey -> SetupState.Creating
is SecureBackupSetupStateMachine.State.KeyCreated -> SetupState.Created(formattedRecoveryKey = key)
is SecureBackupSetupStateMachine.State.KeyCreatedAndSaved -> SetupState.CreatedAndSaved(formattedRecoveryKey = key)
is SecureBackupSetupStateMachine.State.Error -> SetupState.Error(exception)
}
}
@@ -103,13 +105,20 @@ class SecureBackupSetupPresenter @AssistedInject constructor(
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkHasCreatedKey(it))
},
onFailure = {
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkError(it))
if (it is Exception) {
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkError(it))
}
}
)
} else {
observeEncryptionService(stateAndDispatch)
Timber.tag(loggerTagSetup.value).d("Calling encryptionService.enableRecovery()")
encryptionService.enableRecovery(waitForBackupsToUpload = false)
encryptionService.enableRecovery(waitForBackupsToUpload = false).onFailure {
Timber.tag(loggerTagSetup.value).e(it, "Failed to enable recovery")
if (it is Exception) {
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkError(it))
}
}
}
}

View File

@@ -23,6 +23,7 @@ sealed interface SetupState {
data object Creating : SetupState
data class Created(val formattedRecoveryKey: String) : SetupState
data class CreatedAndSaved(val formattedRecoveryKey: String) : SetupState
data class Error(val exception: Exception) : SetupState
}
fun SetupState.recoveryKey(): String? = when (this) {

View File

@@ -26,8 +26,8 @@ class SecureBackupSetupStateMachine @Inject constructor() : FlowReduxStateMachin
}
}
inState<State.CreatingKey> {
on { _: Event.SdkError, state: MachineState<State.CreatingKey> ->
state.override { State.Initial }
on { event: Event.SdkError, state: MachineState<State.CreatingKey> ->
state.override { State.Error(event.exception) }
}
on { event: Event.SdkHasCreatedKey, state: MachineState<State.CreatingKey> ->
state.override { State.KeyCreated(event.key) }
@@ -38,6 +38,11 @@ class SecureBackupSetupStateMachine @Inject constructor() : FlowReduxStateMachin
state.override { State.KeyCreatedAndSaved(state.snapshot.key) }
}
}
inState<State.Error> {
on { _: Event.ClearError, state: MachineState<State.Error> ->
state.override { State.Initial }
}
}
inState<State.KeyCreatedAndSaved> {
}
}
@@ -48,12 +53,14 @@ class SecureBackupSetupStateMachine @Inject constructor() : FlowReduxStateMachin
data object CreatingKey : State
data class KeyCreated(val key: String) : State
data class KeyCreatedAndSaved(val key: String) : State
data class Error(val exception: Exception) : State
}
sealed interface Event {
data object UserCreatesKey : Event
data class SdkHasCreatedKey(val key: String) : Event
data class SdkError(val throwable: Throwable) : Event
data class SdkError(val exception: Exception) : Event
data object UserSavedKey : Event
data object ClearError : Event
}
}

View File

@@ -23,6 +23,7 @@ open class SecureBackupSetupStateProvider : PreviewParameterProvider<SecureBacku
setupState = SetupState.CreatedAndSaved(aFormattedRecoveryKey()),
showSaveConfirmationDialog = true,
),
aSecureBackupSetupState(setupState = SetupState.Error(Exception("Test error"))),
// Add other states here
)
}

View File

@@ -24,6 +24,7 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@@ -49,6 +50,16 @@ fun SecureBackupSetupView(
Content(state = state)
}
if (state.setupState is SetupState.Error) {
ErrorDialog(
title = stringResource(id = CommonStrings.common_something_went_wrong),
content = stringResource(id = CommonStrings.common_something_went_wrong_message),
onSubmit = {
state.eventSink.invoke(SecureBackupSetupEvents.DismissDialog)
},
)
}
if (state.showSaveConfirmationDialog) {
ConfirmationDialog(
title = stringResource(id = R.string.screen_recovery_key_setup_confirmation_title),
@@ -70,7 +81,8 @@ private fun SecureBackupSetupState.canGoBack(): Boolean {
private fun title(state: SecureBackupSetupState): String {
return when (state.setupState) {
SetupState.Init,
SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory) {
SetupState.Creating,
is SetupState.Error -> if (state.isChangeRecoveryKeyUserStory) {
stringResource(id = R.string.screen_recovery_key_change_title)
} else {
stringResource(id = R.string.screen_recovery_key_setup_title)
@@ -85,7 +97,8 @@ private fun title(state: SecureBackupSetupState): String {
private fun subtitle(state: SecureBackupSetupState): String {
return when (state.setupState) {
SetupState.Init,
SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory) {
SetupState.Creating,
is SetupState.Error -> if (state.isChangeRecoveryKeyUserStory) {
stringResource(id = R.string.screen_recovery_key_change_description)
} else {
stringResource(id = R.string.screen_recovery_key_setup_description)
@@ -137,7 +150,8 @@ private fun ColumnScope.Buttons(
val chooserTitle = stringResource(id = R.string.screen_recovery_key_save_action)
when (state.setupState) {
SetupState.Init,
SetupState.Creating -> {
SetupState.Creating,
is SetupState.Error -> {
Button(
text = stringResource(id = CommonStrings.action_done),
enabled = false,

View File

@@ -109,6 +109,34 @@ class SecureBackupSetupPresenterTest {
}
}
@Test
fun `present - handle errors`() = runTest {
val encryptionService = FakeEncryptionService(
enableRecoveryLambda = { Result.failure(IllegalStateException("Test error")) }
)
val presenter = createSecureBackupSetupPresenter(
isChangeRecoveryKeyUserStory = false,
encryptionService = encryptionService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isChangeRecoveryKeyUserStory).isFalse()
assertThat(initialState.setupState).isEqualTo(SetupState.Init)
initialState.eventSink(SecureBackupSetupEvents.CreateRecoveryKey)
val creatingState = awaitItem()
assertThat(creatingState.setupState).isEqualTo(SetupState.Creating)
val failedState = awaitItem()
assertThat(failedState.setupState).isInstanceOf(SetupState.Error::class.java)
failedState.eventSink(SecureBackupSetupEvents.DismissDialog)
val finalState = awaitItem()
assertThat(finalState.setupState).isEqualTo(SetupState.Init)
}
}
@Test
fun `present - change recovery key and save it`() = runTest {
val encryptionService = FakeEncryptionService()
@@ -153,7 +181,9 @@ class SecureBackupSetupPresenterTest {
private fun createSecureBackupSetupPresenter(
isChangeRecoveryKeyUserStory: Boolean = false,
encryptionService: EncryptionService = FakeEncryptionService(),
encryptionService: EncryptionService = FakeEncryptionService(
enableRecoveryLambda = { Result.success(Unit) },
),
): SecureBackupSetupPresenter {
return SecureBackupSetupPresenter(
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,