Crypto: iterate on wording, UI and UX.
Change wording of setup recovery key banner and change target to root. Iterate on wording of encryption screen. Change button to Switch. Iterate on wording to delete key storage. Iterate on wording and icon on the root setting. Remove confirmation dialog when disabling backup. Add subtitle to change recovery key action. Enable key storage directly, remove quite empty screen to setup the backup. Disable recovery action if key backup is disabled.
This commit is contained in:
@@ -260,7 +260,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onSetUpRecoveryClick() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.SetUpRecovery))
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
|
||||
}
|
||||
|
||||
override fun onSessionConfirmRecoveryKeyClick() {
|
||||
|
||||
@@ -138,8 +138,8 @@ private fun ColumnScope.ManageAppSection(
|
||||
}
|
||||
if (state.showSecureBackup) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.KeySolid())),
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_encryption)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Key())),
|
||||
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
|
||||
onClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ internal fun SetUpRecoveryKeyBanner(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.banner_set_up_recovery_title),
|
||||
content = stringResource(R.string.banner_set_up_recovery_content),
|
||||
actionText = stringResource(R.string.banner_set_up_recovery_submit),
|
||||
onSubmitClick = onContinueClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<string name="banner_migrate_to_native_sliding_sync_description">"Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_title">"Upgrade available"</string>
|
||||
<string name="banner_set_up_recovery_content">"Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."</string>
|
||||
<string name="banner_set_up_recovery_title">"Set up recovery"</string>
|
||||
<string name="banner_set_up_recovery_content">"Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Set up recovery"</string>
|
||||
<string name="banner_set_up_recovery_title">"Set up recovery to protect your account"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
||||
<string name="full_screen_intent_banner_message">"To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."</string>
|
||||
|
||||
@@ -121,12 +121,9 @@ class RoomListViewTest {
|
||||
),
|
||||
onSetUpRecoveryClick = callback,
|
||||
)
|
||||
|
||||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
|
||||
rule.clickOn(R.string.banner_set_up_recovery_submit)
|
||||
eventsRecorder.assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||
import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
|
||||
import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
|
||||
import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
|
||||
import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode
|
||||
import io.element.android.features.securebackup.impl.root.SecureBackupRootNode
|
||||
@@ -63,9 +62,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||
@Parcelize
|
||||
data object Disable : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Enable : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object EnterRecoveryKey : NavTarget
|
||||
|
||||
@@ -91,10 +87,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||
backstack.push(NavTarget.Disable)
|
||||
}
|
||||
|
||||
override fun onEnableClick() {
|
||||
backstack.push(NavTarget.Enable)
|
||||
}
|
||||
|
||||
override fun onConfirmRecoveryKeyClick() {
|
||||
backstack.push(NavTarget.EnterRecoveryKey)
|
||||
}
|
||||
@@ -116,9 +108,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||
NavTarget.Disable -> {
|
||||
createNode<SecureBackupDisableNode>(buildContext)
|
||||
}
|
||||
NavTarget.Enable -> {
|
||||
createNode<SecureBackupEnableNode>(buildContext)
|
||||
}
|
||||
NavTarget.EnterRecoveryKey -> {
|
||||
val callback = object : SecureBackupEnterRecoveryKeyNode.Callback {
|
||||
override fun onEnterRecoveryKeySuccess() {
|
||||
|
||||
@@ -37,11 +37,7 @@ class SecureBackupDisablePresenter @Inject constructor(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun handleEvents(event: SecureBackupDisableEvents) {
|
||||
when (event) {
|
||||
is SecureBackupDisableEvents.DisableBackup -> if (disableAction.value.isConfirming()) {
|
||||
coroutineScope.disableBackup(disableAction)
|
||||
} else {
|
||||
disableAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
is SecureBackupDisableEvents.DisableBackup -> coroutineScope.disableBackup(disableAction)
|
||||
SecureBackupDisableEvents.DismissDialogs -> {
|
||||
disableAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.features.securebackup.impl.disable
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
@@ -25,7 +26,6 @@ import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
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
|
||||
@@ -44,7 +44,7 @@ fun SecureBackupDisableView(
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = R.string.screen_key_backup_disable_title),
|
||||
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeyOffSolid()),
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
buttons = { Buttons(state = state) },
|
||||
) {
|
||||
Content(state = state)
|
||||
@@ -52,12 +52,6 @@ fun SecureBackupDisableView(
|
||||
|
||||
AsyncActionView(
|
||||
async = state.disableAction,
|
||||
confirmationDialog = {
|
||||
SecureBackupDisableConfirmationDialog(
|
||||
onConfirm = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup) },
|
||||
onDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) },
|
||||
)
|
||||
},
|
||||
progressDialog = {},
|
||||
errorMessage = { it.message ?: it.toString() },
|
||||
onErrorDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) },
|
||||
@@ -65,18 +59,6 @@ fun SecureBackupDisableView(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecureBackupDisableConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(id = R.string.screen_key_backup_disable_confirmation_title),
|
||||
content = stringResource(id = R.string.screen_key_backup_disable_confirmation_description),
|
||||
submitText = stringResource(id = R.string.screen_key_backup_disable_confirmation_action_turn_off),
|
||||
destructiveSubmit = true,
|
||||
onSubmitClick = onConfirm,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Buttons(
|
||||
state: SecureBackupDisableState,
|
||||
@@ -105,15 +87,20 @@ private fun Content(state: SecureBackupDisableState) {
|
||||
|
||||
@Composable
|
||||
private fun SecureBackupDisableItem(text: String) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = ElementTheme.colors.bgActionSecondaryHovered)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp, end = 4.dp),
|
||||
text = text,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
sealed interface SecureBackupEnableEvents {
|
||||
data object EnableBackup : SecureBackupEnableEvents
|
||||
data object DismissDialog : SecureBackupEnableEvents
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class SecureBackupEnableNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: SecureBackupEnablePresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
SecureBackupEnableView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onSuccess = ::navigateUp,
|
||||
onBackClick = ::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.securebackup.impl.loggerTagDisable
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class SecureBackupEnablePresenter @Inject constructor(
|
||||
private val encryptionService: EncryptionService,
|
||||
) : Presenter<SecureBackupEnableState> {
|
||||
@Composable
|
||||
override fun present(): SecureBackupEnableState {
|
||||
val enableAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun handleEvents(event: SecureBackupEnableEvents) {
|
||||
when (event) {
|
||||
is SecureBackupEnableEvents.EnableBackup ->
|
||||
coroutineScope.enableBackup(enableAction)
|
||||
SecureBackupEnableEvents.DismissDialog -> {
|
||||
enableAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SecureBackupEnableState(
|
||||
enableAction = enableAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.enableBackup(action: MutableState<AsyncAction<Unit>>) = launch {
|
||||
suspend {
|
||||
Timber.tag(loggerTagDisable.value).d("Calling encryptionService.enableBackups()")
|
||||
encryptionService.enableBackups().getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class SecureBackupEnableState(
|
||||
val enableAction: AsyncAction<Unit>,
|
||||
val eventSink: (SecureBackupEnableEvents) -> Unit
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
open class SecureBackupEnableStateProvider : PreviewParameterProvider<SecureBackupEnableState> {
|
||||
override val values: Sequence<SecureBackupEnableState>
|
||||
get() = sequenceOf(
|
||||
aSecureBackupEnableState(),
|
||||
aSecureBackupEnableState(enableAction = AsyncAction.Loading),
|
||||
aSecureBackupEnableState(enableAction = AsyncAction.Failure(Exception("Failed to enable"))),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
||||
fun aSecureBackupEnableState(
|
||||
enableAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = SecureBackupEnableState(
|
||||
enableAction = enableAction,
|
||||
eventSink = {}
|
||||
)
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun SecureBackupEnableView(
|
||||
state: SecureBackupEnableState,
|
||||
onSuccess: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
||||
buttons = { Buttons(state = state) }
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.enableAction,
|
||||
progressDialog = { },
|
||||
onSuccess = { onSuccess() },
|
||||
onErrorDismiss = { state.eventSink.invoke(SecureBackupEnableEvents.DismissDialog) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Buttons(
|
||||
state: SecureBackupEnableState,
|
||||
) {
|
||||
Button(
|
||||
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
||||
showProgress = state.enableAction.isLoading(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) }
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SecureBackupEnableViewPreview(
|
||||
@PreviewParameter(SecureBackupEnableStateProvider::class) state: SecureBackupEnableState
|
||||
) = ElementPreview {
|
||||
SecureBackupEnableView(
|
||||
state = state,
|
||||
onSuccess = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
@@ -9,4 +9,7 @@ package io.element.android.features.securebackup.impl.root
|
||||
|
||||
sealed interface SecureBackupRootEvents {
|
||||
data object RetryKeyBackupState : SecureBackupRootEvents
|
||||
data object EnableKeyStorage : SecureBackupRootEvents
|
||||
data object DisplayKeyStorageDisabledError : SecureBackupRootEvents
|
||||
data object DismissDialog : SecureBackupRootEvents
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ class SecureBackupRootNode @AssistedInject constructor(
|
||||
fun onSetupClick()
|
||||
fun onChangeClick()
|
||||
fun onDisableClick()
|
||||
fun onEnableClick()
|
||||
fun onConfirmRecoveryKeyClick()
|
||||
}
|
||||
|
||||
@@ -50,10 +49,6 @@ class SecureBackupRootNode @AssistedInject constructor(
|
||||
plugins<Callback>().forEach { it.onDisableClick() }
|
||||
}
|
||||
|
||||
private fun onEnableClick() {
|
||||
plugins<Callback>().forEach { it.onEnableClick() }
|
||||
}
|
||||
|
||||
private fun onConfirmRecoveryKeyClick() {
|
||||
plugins<Callback>().forEach { it.onConfirmRecoveryKeyClick() }
|
||||
}
|
||||
@@ -71,7 +66,6 @@ class SecureBackupRootNode @AssistedInject constructor(
|
||||
onBackClick = ::navigateUp,
|
||||
onSetupClick = ::onSetupClick,
|
||||
onChangeClick = ::onChangeClick,
|
||||
onEnableClick = ::onEnableClick,
|
||||
onDisableClick = ::onDisableClick,
|
||||
onConfirmRecoveryKeyClick = ::onConfirmRecoveryKeyClick,
|
||||
onLearnMoreClick = { onLearnMoreClick(uriHandler) },
|
||||
|
||||
@@ -15,7 +15,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.securebackup.impl.loggerTagDisable
|
||||
import io.element.android.features.securebackup.impl.loggerTagRoot
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
@@ -41,7 +44,8 @@ class SecureBackupRootPresenter @Inject constructor(
|
||||
|
||||
val backupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||
|
||||
val enableAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
var displayKeyStorageDisabledError by remember { mutableStateOf(false) }
|
||||
Timber.tag(loggerTagRoot.value).d("backupState: $backupState")
|
||||
Timber.tag(loggerTagRoot.value).d("recoveryState: $recoveryState")
|
||||
|
||||
@@ -56,14 +60,22 @@ class SecureBackupRootPresenter @Inject constructor(
|
||||
fun handleEvents(event: SecureBackupRootEvents) {
|
||||
when (event) {
|
||||
SecureBackupRootEvents.RetryKeyBackupState -> localCoroutineScope.getKeyBackupStatus(doesBackupExistOnServerAction)
|
||||
SecureBackupRootEvents.EnableKeyStorage -> localCoroutineScope.enableBackup(enableAction)
|
||||
SecureBackupRootEvents.DismissDialog -> {
|
||||
enableAction.value = AsyncAction.Uninitialized
|
||||
displayKeyStorageDisabledError = false
|
||||
}
|
||||
SecureBackupRootEvents.DisplayKeyStorageDisabledError -> displayKeyStorageDisabledError = true
|
||||
}
|
||||
}
|
||||
|
||||
return SecureBackupRootState(
|
||||
enableAction = enableAction.value,
|
||||
backupState = backupState,
|
||||
doesBackupExistOnServer = doesBackupExistOnServerAction.value,
|
||||
recoveryState = recoveryState,
|
||||
appName = buildMeta.applicationName,
|
||||
displayKeyStorageDisabledError = displayKeyStorageDisabledError,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
@@ -74,4 +86,11 @@ class SecureBackupRootPresenter @Inject constructor(
|
||||
encryptionService.doesBackupExistOnServer().getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.enableBackup(action: MutableState<AsyncAction<Unit>>) = launch {
|
||||
suspend {
|
||||
Timber.tag(loggerTagDisable.value).d("Calling encryptionService.enableBackups()")
|
||||
encryptionService.enableBackups().getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,31 @@
|
||||
|
||||
package io.element.android.features.securebackup.impl.root
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
|
||||
data class SecureBackupRootState(
|
||||
val enableAction: AsyncAction<Unit>,
|
||||
val backupState: BackupState,
|
||||
val doesBackupExistOnServer: AsyncData<Boolean>,
|
||||
val recoveryState: RecoveryState,
|
||||
val appName: String,
|
||||
val displayKeyStorageDisabledError: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val eventSink: (SecureBackupRootEvents) -> Unit,
|
||||
)
|
||||
) {
|
||||
val isKeyStorageEnabled: Boolean
|
||||
get() = when (backupState) {
|
||||
BackupState.UNKNOWN -> doesBackupExistOnServer.dataOrNull() == true
|
||||
BackupState.CREATING,
|
||||
BackupState.ENABLING,
|
||||
BackupState.RESUMING,
|
||||
BackupState.DOWNLOADING,
|
||||
BackupState.ENABLED -> true
|
||||
BackupState.WAITING_FOR_SYNC,
|
||||
BackupState.DISABLING -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package io.element.android.features.securebackup.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
@@ -22,28 +23,47 @@ open class SecureBackupRootStateProvider : PreviewParameterProvider<SecureBackup
|
||||
aSecureBackupRootState(backupState = BackupState.UNKNOWN, doesBackupExistOnServer = AsyncData.Failure(Exception("An error"))),
|
||||
aSecureBackupRootState(backupState = BackupState.WAITING_FOR_SYNC),
|
||||
aSecureBackupRootState(backupState = BackupState.CREATING),
|
||||
aSecureBackupRootState(
|
||||
backupState = BackupState.CREATING,
|
||||
enableAction = AsyncAction.Failure(Exception("Error")),
|
||||
),
|
||||
aSecureBackupRootState(backupState = BackupState.ENABLING),
|
||||
aSecureBackupRootState(backupState = BackupState.RESUMING),
|
||||
aSecureBackupRootState(backupState = BackupState.DOWNLOADING),
|
||||
aSecureBackupRootState(backupState = BackupState.DISABLING),
|
||||
aSecureBackupRootState(backupState = BackupState.ENABLED),
|
||||
aSecureBackupRootState(recoveryState = RecoveryState.UNKNOWN),
|
||||
aSecureBackupRootState(recoveryState = RecoveryState.ENABLED),
|
||||
aSecureBackupRootState(recoveryState = RecoveryState.DISABLED),
|
||||
aSecureBackupRootState(recoveryState = RecoveryState.INCOMPLETE),
|
||||
// Add other states here
|
||||
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.UNKNOWN),
|
||||
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.ENABLED),
|
||||
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.DISABLED),
|
||||
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.INCOMPLETE),
|
||||
aSecureBackupRootState(
|
||||
backupState = BackupState.UNKNOWN,
|
||||
doesBackupExistOnServer = AsyncData.Success(false),
|
||||
recoveryState = RecoveryState.ENABLED,
|
||||
),
|
||||
aSecureBackupRootState(
|
||||
backupState = BackupState.UNKNOWN,
|
||||
doesBackupExistOnServer = AsyncData.Success(false),
|
||||
recoveryState = RecoveryState.ENABLED,
|
||||
displayKeyStorageDisabledError = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aSecureBackupRootState(
|
||||
enableAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
backupState: BackupState = BackupState.UNKNOWN,
|
||||
doesBackupExistOnServer: AsyncData<Boolean> = AsyncData.Uninitialized,
|
||||
recoveryState: RecoveryState = RecoveryState.UNKNOWN,
|
||||
displayKeyStorageDisabledError: Boolean = false,
|
||||
snackbarMessage: SnackbarMessage? = null,
|
||||
) = SecureBackupRootState(
|
||||
enableAction = enableAction,
|
||||
backupState = backupState,
|
||||
doesBackupExistOnServer = doesBackupExistOnServer,
|
||||
recoveryState = recoveryState,
|
||||
appName = "Element",
|
||||
displayKeyStorageDisabledError = displayKeyStorageDisabledError,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
@@ -7,28 +7,27 @@
|
||||
|
||||
package io.element.android.features.securebackup.impl.root
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
@@ -41,7 +40,6 @@ fun SecureBackupRootView(
|
||||
onBackClick: () -> Unit,
|
||||
onSetupClick: () -> Unit,
|
||||
onChangeClick: () -> Unit,
|
||||
onEnableClick: () -> Unit,
|
||||
onDisableClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
@@ -52,122 +50,186 @@ fun SecureBackupRootView(
|
||||
PreferencePage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = CommonStrings.common_chat_backup),
|
||||
title = stringResource(id = CommonStrings.common_encryption),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) {
|
||||
val text = buildAnnotatedStringWithStyledPart(
|
||||
fullTextRes = R.string.screen_chat_backup_key_backup_description,
|
||||
coloredTextRes = CommonStrings.action_learn_more,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
underline = false,
|
||||
bold = true,
|
||||
)
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_title),
|
||||
subtitleAnnotated = text,
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_key_backup_title),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = buildAnnotatedStringWithStyledPart(
|
||||
fullTextRes = R.string.screen_chat_backup_key_backup_description,
|
||||
coloredTextRes = CommonStrings.action_learn_more,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
underline = false,
|
||||
bold = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = onLearnMoreClick,
|
||||
)
|
||||
|
||||
// Disable / Enable backup
|
||||
when (state.backupState) {
|
||||
BackupState.WAITING_FOR_SYNC -> Unit
|
||||
BackupState.UNKNOWN -> {
|
||||
when (state.doesBackupExistOnServer) {
|
||||
is AsyncData.Success -> when (state.doesBackupExistOnServer.data) {
|
||||
true -> {
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
|
||||
tintColor = ElementTheme.colors.textCriticalPrimary,
|
||||
onClick = onDisableClick,
|
||||
// Disable / Enable key storage
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_key_storage_toggle_title),
|
||||
)
|
||||
},
|
||||
trailingContent = when (state.backupState) {
|
||||
BackupState.WAITING_FOR_SYNC,
|
||||
BackupState.DISABLING -> ListItemContent.Custom { LoadingView() }
|
||||
BackupState.UNKNOWN -> {
|
||||
when (state.doesBackupExistOnServer) {
|
||||
is AsyncData.Success -> {
|
||||
ListItemContent.Switch(checked = state.doesBackupExistOnServer.data)
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> ListItemContent.Custom { LoadingView() }
|
||||
is AsyncData.Failure -> ListItemContent.Custom {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.action_retry)
|
||||
)
|
||||
}
|
||||
false -> {
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
||||
onClick = onEnableClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> {
|
||||
ListItem(headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
})
|
||||
}
|
||||
is AsyncData.Failure -> {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.error_unknown),
|
||||
)
|
||||
},
|
||||
trailingContent = ListItemContent.Custom {
|
||||
TextButton(
|
||||
text = stringResource(
|
||||
id = CommonStrings.action_retry
|
||||
),
|
||||
onClick = { state.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
||||
onClick = onEnableClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
BackupState.CREATING,
|
||||
BackupState.ENABLING,
|
||||
BackupState.RESUMING,
|
||||
BackupState.ENABLED,
|
||||
BackupState.DOWNLOADING -> {
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
|
||||
tintColor = ElementTheme.colors.textCriticalPrimary,
|
||||
onClick = onDisableClick,
|
||||
)
|
||||
}
|
||||
BackupState.DISABLING -> {
|
||||
AsyncLoading()
|
||||
}
|
||||
}
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
BackupState.CREATING,
|
||||
BackupState.ENABLING,
|
||||
BackupState.RESUMING,
|
||||
BackupState.ENABLED,
|
||||
BackupState.DOWNLOADING -> ListItemContent.Switch(checked = true)
|
||||
},
|
||||
onClick = {
|
||||
when (state.backupState) {
|
||||
BackupState.WAITING_FOR_SYNC,
|
||||
BackupState.DISABLING -> Unit
|
||||
BackupState.UNKNOWN -> {
|
||||
when (state.doesBackupExistOnServer) {
|
||||
is AsyncData.Success -> {
|
||||
if (state.doesBackupExistOnServer.data) {
|
||||
onDisableClick()
|
||||
} else {
|
||||
state.eventSink.invoke(SecureBackupRootEvents.EnableKeyStorage)
|
||||
}
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> Unit
|
||||
is AsyncData.Failure -> state.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState)
|
||||
}
|
||||
}
|
||||
BackupState.CREATING,
|
||||
BackupState.ENABLING,
|
||||
BackupState.RESUMING,
|
||||
BackupState.ENABLED,
|
||||
BackupState.DOWNLOADING -> onDisableClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
// Setup recovery
|
||||
when (state.recoveryState) {
|
||||
RecoveryState.UNKNOWN,
|
||||
RecoveryState.WAITING_FOR_SYNC -> Unit
|
||||
RecoveryState.DISABLED -> {
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_recovery_action_setup),
|
||||
subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_setup_description, state.appName),
|
||||
onClick = onSetupClick,
|
||||
showEndBadge = true,
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_recovery_action_setup),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_recovery_action_setup_description, state.appName),
|
||||
)
|
||||
},
|
||||
trailingContent = ListItemContent.Badge,
|
||||
enabled = state.isKeyStorageEnabled,
|
||||
alwaysClickable = true,
|
||||
onClick = {
|
||||
if (state.isKeyStorageEnabled) {
|
||||
onSetupClick()
|
||||
} else {
|
||||
state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
RecoveryState.ENABLED -> {
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_recovery_action_change),
|
||||
onClick = onChangeClick,
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_recovery_action_change),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_recovery_action_change_description),
|
||||
)
|
||||
},
|
||||
enabled = state.isKeyStorageEnabled,
|
||||
alwaysClickable = true,
|
||||
onClick = {
|
||||
if (state.isKeyStorageEnabled) {
|
||||
onChangeClick()
|
||||
} else {
|
||||
state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
RecoveryState.INCOMPLETE ->
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm),
|
||||
subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm_description),
|
||||
showEndBadge = true,
|
||||
onClick = onConfirmRecoveryKeyClick,
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm_description),
|
||||
)
|
||||
},
|
||||
trailingContent = ListItemContent.Badge,
|
||||
enabled = state.isKeyStorageEnabled,
|
||||
alwaysClickable = true,
|
||||
onClick = {
|
||||
if (state.isKeyStorageEnabled) {
|
||||
onConfirmRecoveryKeyClick()
|
||||
} else {
|
||||
state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncActionView(
|
||||
async = state.enableAction,
|
||||
progressDialog = { },
|
||||
onSuccess = { },
|
||||
onErrorDismiss = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) }
|
||||
)
|
||||
if (state.displayKeyStorageDisabledError) {
|
||||
ErrorDialog(
|
||||
title = null,
|
||||
content = stringResource(id = R.string.screen_chat_backup_key_storage_disabled_error),
|
||||
onSubmit = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingView() {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.progressSemantics()
|
||||
.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@@ -180,7 +242,6 @@ internal fun SecureBackupRootViewPreview(
|
||||
onBackClick = {},
|
||||
onSetupClick = {},
|
||||
onChangeClick = {},
|
||||
onEnableClick = {},
|
||||
onDisableClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onLearnMoreClick = {},
|
||||
|
||||
@@ -91,14 +91,14 @@ private fun RecoveryKeyStaticContent(
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
.clickableIfNotNull(onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
.clickableIfNotNull(onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (state.formattedRecoveryKey != null) {
|
||||
@@ -116,15 +116,15 @@ private fun RecoveryKeyStaticContent(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 11.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 11.dp)
|
||||
) {
|
||||
if (state.inProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.progressSemantics()
|
||||
.padding(end = 8.dp)
|
||||
.size(16.dp),
|
||||
.progressSemantics()
|
||||
.padding(end = 8.dp)
|
||||
.size(16.dp),
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
strokeWidth = 1.5.dp,
|
||||
)
|
||||
@@ -161,12 +161,12 @@ private fun RecoveryKeyFormContent(
|
||||
}
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.recoveryKey)
|
||||
.autofill(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onChange(it) },
|
||||
),
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.recoveryKey)
|
||||
.autofill(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onChange(it) },
|
||||
),
|
||||
minLines = 2,
|
||||
value = state.formattedRecoveryKey.orEmpty(),
|
||||
onValueChange = onChange,
|
||||
@@ -189,30 +189,18 @@ private fun RecoveryKeyFooter(state: RecoveryKeyViewState) {
|
||||
RecoveryKeyUserStory.Setup,
|
||||
RecoveryKeyUserStory.Change -> {
|
||||
if (state.formattedRecoveryKey == null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.InfoSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.size(20.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = if (state.recoveryKeyUserStory == RecoveryKeyUserStory.Change) {
|
||||
R.string.screen_recovery_key_change_generate_key_description
|
||||
} else {
|
||||
R.string.screen_recovery_key_setup_generate_key_description
|
||||
}
|
||||
),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = if (state.recoveryKeyUserStory == RecoveryKeyUserStory.Change) {
|
||||
R.string.screen_recovery_key_change_generate_key_description
|
||||
} else {
|
||||
R.string.screen_recovery_key_setup_generate_key_description
|
||||
}
|
||||
),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_recovery_key_save_key_description),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_chat_backup_key_backup_action_disable">"Turn off backup"</string>
|
||||
<string name="screen_chat_backup_key_backup_action_disable">"Delete key storage"</string>
|
||||
<string name="screen_chat_backup_key_backup_action_enable">"Turn on backup"</string>
|
||||
<string name="screen_chat_backup_key_backup_description">"Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$s."</string>
|
||||
<string name="screen_chat_backup_key_backup_title">"Key storage"</string>
|
||||
<string name="screen_chat_backup_key_storage_disabled_error">"Key storage must be turned on to set up recovery."</string>
|
||||
<string name="screen_chat_backup_key_storage_toggle_description">"Upload keys from this device"</string>
|
||||
<string name="screen_chat_backup_key_storage_toggle_title">"Allow key storage"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change">"Change recovery key"</string>
|
||||
@@ -28,10 +29,10 @@
|
||||
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Turn off"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_description">"You will lose your encrypted messages if you are signed out of all devices."</string>
|
||||
<string name="screen_key_backup_disable_confirmation_title">"Are you sure you want to turn off backup?"</string>
|
||||
<string name="screen_key_backup_disable_description">"Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"</string>
|
||||
<string name="screen_key_backup_disable_description_point_1">"Not have encrypted message history on new devices"</string>
|
||||
<string name="screen_key_backup_disable_description_point_2">"Lose access to your encrypted messages if you are signed out of %1$s everywhere"</string>
|
||||
<string name="screen_key_backup_disable_title">"Are you sure you want to turn off backup?"</string>
|
||||
<string name="screen_key_backup_disable_description">"Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"</string>
|
||||
<string name="screen_key_backup_disable_description_point_1">"You will not have encrypted message history on new devices"</string>
|
||||
<string name="screen_key_backup_disable_description_point_2">"You will lose access to your encrypted messages if you are signed out of %1$s everywhere"</string>
|
||||
<string name="screen_key_backup_disable_title">"Are you sure you want to turn off key storage and delete it?"</string>
|
||||
<string name="screen_recovery_key_change_description">"Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work."</string>
|
||||
<string name="screen_recovery_key_change_generate_key">"Generate a new recovery key"</string>
|
||||
<string name="screen_recovery_key_change_generate_key_description">"Do not share this with anyone!"</string>
|
||||
@@ -54,7 +55,7 @@
|
||||
<string name="screen_recovery_key_save_title">"Save your recovery key somewhere safe"</string>
|
||||
<string name="screen_recovery_key_setup_confirmation_description">"You will not be able to access your new recovery key after this step."</string>
|
||||
<string name="screen_recovery_key_setup_confirmation_title">"Have you saved your recovery key?"</string>
|
||||
<string name="screen_recovery_key_setup_description">"Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."</string>
|
||||
<string name="screen_recovery_key_setup_description">"Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’."</string>
|
||||
<string name="screen_recovery_key_setup_generate_key">"Generate your recovery key"</string>
|
||||
<string name="screen_recovery_key_setup_generate_key_description">"Do not share this with anyone!"</string>
|
||||
<string name="screen_recovery_key_setup_success">"Recovery setup successful"</string>
|
||||
|
||||
@@ -38,22 +38,6 @@ class SecureBackupDisablePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user delete backup and cancel`() = runTest {
|
||||
val presenter = createSecureBackupDisablePresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
||||
val state = awaitItem()
|
||||
assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
initialState.eventSink(SecureBackupDisableEvents.DismissDialogs)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user delete backup success`() = runTest {
|
||||
val presenter = createSecureBackupDisablePresenter()
|
||||
@@ -63,9 +47,6 @@ class SecureBackupDisablePresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
||||
val state = awaitItem()
|
||||
assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val finalState = awaitItem()
|
||||
@@ -87,9 +68,6 @@ class SecureBackupDisablePresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
||||
val state = awaitItem()
|
||||
assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SecureBackupEnablePresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.enableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user enable backup`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(SecureBackupEnableEvents.EnableBackup)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.enableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.enableAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - user enable backup with error`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
encryptionService.givenEnableBackupsFailure(AN_EXCEPTION)
|
||||
val presenter = createPresenter(encryptionService = encryptionService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(SecureBackupEnableEvents.EnableBackup)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.enableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.enableAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
||||
errorState.eventSink(SecureBackupEnableEvents.DismissDialog)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.enableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
) = SecureBackupEnablePresenter(
|
||||
encryptionService = encryptionService,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user