Merge pull request #3750 from element-hq/feature/bma/cryptoIteration
UI iteration on the encryption settings
This commit is contained in:
commit
e95613f3fe
100 changed files with 466 additions and 666 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ 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.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
|
|
@ -38,6 +39,8 @@ class SecureBackupRootPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
|
||||
assertThat(initialState.doesBackupExistOnServer.dataOrNull()).isTrue()
|
||||
assertThat(initialState.enableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(initialState.displayKeyStorageDisabledError).isFalse()
|
||||
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
|
||||
assertThat(initialState.appName).isEqualTo("Element")
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
|
|
@ -70,6 +73,35 @@ class SecureBackupRootPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - setting up encryption when key storage is disabled should emit a state to render a dialog`() = runTest {
|
||||
val presenter = createSecureBackupRootPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||
assertThat(awaitItem().displayKeyStorageDisabledError).isTrue()
|
||||
initialState.eventSink(SecureBackupRootEvents.DismissDialog)
|
||||
assertThat(awaitItem().displayKeyStorageDisabledError).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enable key storage invoke the expected API`() = runTest {
|
||||
val presenter = createSecureBackupRootPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(SecureBackupRootEvents.EnableKeyStorage)
|
||||
assertThat(awaitItem().enableAction.isLoading()).isTrue()
|
||||
assertThat(awaitItem().enableAction.isSuccess()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSecureBackupRootPresenter(
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
appName: String = "Element",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue