mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-8202 move dialog status to VM for restore item, add check for MP p… (#3436)
This commit is contained in:
parent
dbf1d423e8
commit
27747b6cb9
4 changed files with 369 additions and 269 deletions
|
@ -15,9 +15,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
@ -69,11 +67,6 @@ fun VaultItemScreen(
|
|||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val confirmRestoreAction = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.ConfirmRestoreClick) }
|
||||
}
|
||||
|
||||
var pendingRestoreCipher by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val fileChooserLauncher = intentManager.getActivityResultLauncher { activityResult ->
|
||||
intentManager.getFileDataFromActivityResult(activityResult)
|
||||
|
@ -150,27 +143,13 @@ fun VaultItemScreen(
|
|||
)
|
||||
}
|
||||
},
|
||||
onConfirmRestoreAction = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(VaultItemAction.Common.ConfirmRestoreClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (pendingRestoreCipher) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.restore),
|
||||
message = stringResource(id = R.string.do_you_really_want_to_restore_cipher),
|
||||
confirmButtonText = stringResource(id = R.string.ok),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = {
|
||||
pendingRestoreCipher = false
|
||||
confirmRestoreAction()
|
||||
},
|
||||
onDismissClick = {
|
||||
pendingRestoreCipher = false
|
||||
},
|
||||
onDismissRequest = {
|
||||
pendingRestoreCipher = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
|
@ -189,7 +168,13 @@ fun VaultItemScreen(
|
|||
if (state.isCipherDeleted) {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.restore),
|
||||
onClick = { pendingRestoreCipher = true },
|
||||
onClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.RestoreVaultItemClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag("RestoreButton"),
|
||||
)
|
||||
}
|
||||
|
@ -298,6 +283,7 @@ private fun VaultItemDialogs(
|
|||
onConfirmDeleteClick: () -> Unit,
|
||||
onSubmitMasterPassword: (masterPassword: String, action: PasswordRepromptAction) -> Unit,
|
||||
onConfirmCloneWithoutFido2Credential: () -> Unit,
|
||||
onConfirmRestoreAction: () -> Unit,
|
||||
) {
|
||||
when (dialog) {
|
||||
is VaultItemState.DialogState.Generic -> BitwardenBasicDialog(
|
||||
|
@ -343,6 +329,16 @@ private fun VaultItemDialogs(
|
|||
)
|
||||
}
|
||||
|
||||
VaultItemState.DialogState.RestoreItemDialog -> BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.restore),
|
||||
message = stringResource(id = R.string.do_you_really_want_to_restore_cipher),
|
||||
confirmButtonText = stringResource(id = R.string.ok),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = onConfirmRestoreAction,
|
||||
onDismissClick = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,6 +165,8 @@ class VaultItemViewModel @Inject constructor(
|
|||
is VaultItemAction.Common.ConfirmCloneWithoutFido2CredentialClick -> {
|
||||
handleConfirmCloneClick()
|
||||
}
|
||||
|
||||
is VaultItemAction.Common.RestoreVaultItemClick -> handleRestoreItemClicked()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,28 +175,24 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleDismissDialogClick() {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
dismissDialog()
|
||||
}
|
||||
|
||||
private fun handleDeleteClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState
|
||||
.DialogState
|
||||
.DeleteConfirmationPrompt(state.deletionConfirmationText),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState
|
||||
.DialogState
|
||||
.DeleteConfirmationPrompt(state.deletionConfirmationText),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -202,13 +200,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handleEditClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.EditClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.EditClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(
|
||||
|
@ -221,9 +217,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleMasterPasswordSubmit(action: VaultItemAction.Common.MasterPasswordSubmit) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
||||
}
|
||||
updateDialogState(VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.validatePassword(action.masterPassword)
|
||||
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result, action.action))
|
||||
|
@ -240,13 +234,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(action.field),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(action.field),
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
clipboardManager.setText(text = action.field)
|
||||
|
@ -269,16 +261,14 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewHiddenFieldClicked(
|
||||
field = action.field,
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewHiddenFieldClicked(
|
||||
field = action.field,
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
|
@ -311,23 +301,17 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentDownloadClick(
|
||||
attachment = action.attachment,
|
||||
),
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentDownloadClick(
|
||||
attachment = action.attachment,
|
||||
),
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(R.string.downloading.asText()),
|
||||
)
|
||||
}
|
||||
updateDialogState(VaultItemState.DialogState.Loading(R.string.downloading.asText()))
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = vaultRepository
|
||||
|
@ -349,9 +333,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handleAttachmentFileLocationReceive(
|
||||
action: VaultItemAction.Common.AttachmentFileLocationReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = null)
|
||||
}
|
||||
dismissDialog()
|
||||
|
||||
val file = temporaryAttachmentData ?: return
|
||||
viewModelScope.launch {
|
||||
|
@ -374,25 +356,21 @@ class VaultItemViewModel @Inject constructor(
|
|||
temporaryAttachmentData?.let { fileManager.delete(it) }
|
||||
}
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
R.string.unable_to_save_attachment.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
R.string.unable_to_save_attachment.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleAttachmentsClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentsClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentsClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
|
||||
|
@ -403,22 +381,18 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handleCloneClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresCloneConfirmation) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Fido2CredentialCannotBeCopiedConfirmationPrompt(
|
||||
message = R.string.the_passkey_will_not_be_copied_to_the_cloned_item_do_you_want_to_continue_cloning_this_item.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Fido2CredentialCannotBeCopiedConfirmationPrompt(
|
||||
message = R.string.the_passkey_will_not_be_copied_to_the_cloned_item_do_you_want_to_continue_cloning_this_item.asText(),
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
} else if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CloneClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CloneClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToAddEdit(itemId = state.vaultItemId, isClone = true))
|
||||
|
@ -445,13 +419,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handleMoveToOrganizationClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.MoveToOrganizationClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.MoveToOrganizationClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
|
||||
|
@ -464,17 +436,15 @@ class VaultItemViewModel @Inject constructor(
|
|||
|
||||
private fun handleConfirmDeleteClick() {
|
||||
onContent { content ->
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(
|
||||
if (state.isCipherDeleted) {
|
||||
R.string.deleting.asText()
|
||||
} else {
|
||||
R.string.soft_deleting.asText()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Loading(
|
||||
if (state.isCipherDeleted) {
|
||||
R.string.deleting.asText()
|
||||
} else {
|
||||
R.string.soft_deleting.asText()
|
||||
},
|
||||
),
|
||||
)
|
||||
content.common.currentCipher?.let { cipher ->
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
|
@ -497,13 +467,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleConfirmRestoreClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(
|
||||
R.string.restoring.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Loading(
|
||||
R.string.restoring.asText(),
|
||||
),
|
||||
)
|
||||
onContent { content ->
|
||||
content
|
||||
.common
|
||||
|
@ -566,9 +534,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handleCheckForBreachClick() {
|
||||
onLoginContent { _, login ->
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
||||
}
|
||||
updateDialogState(VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.getPasswordBreachCount(password = password)
|
||||
sendAction(VaultItemAction.Internal.PasswordBreachReceive(result))
|
||||
|
@ -580,13 +546,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
onLoginContent { content, login ->
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = password),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = password),
|
||||
),
|
||||
)
|
||||
return@onLoginContent
|
||||
}
|
||||
clipboardManager.setText(text = password)
|
||||
|
@ -623,13 +587,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handlePasswordHistoryClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.PasswordHistoryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.PasswordHistoryClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToPasswordHistory(state.vaultItemId))
|
||||
|
@ -641,15 +603,13 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
onLoginContent { content, login ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewPasswordClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewPasswordClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
return@onLoginContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
|
@ -696,15 +656,13 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
onCardContent { content, card ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewCodeClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewCodeClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
|
@ -732,13 +690,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
onCardContent { content, card ->
|
||||
val number = requireNotNull(card.number).number
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = number),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = number),
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
clipboardManager.setText(text = number)
|
||||
|
@ -749,13 +705,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
onCardContent { content, card ->
|
||||
val securityCode = requireNotNull(card.securityCode).code
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = securityCode),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = securityCode),
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
clipboardManager.setText(text = securityCode)
|
||||
|
@ -767,15 +721,13 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
onCardContent { content, card ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewNumberClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewNumberClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
|
@ -842,9 +794,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.Generic(message = message))
|
||||
}
|
||||
updateDialogState(VaultItemState.DialogState.Generic(message = message))
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
@ -918,7 +868,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
.data
|
||||
?.cipher
|
||||
?.toViewState(
|
||||
previousState = state.viewState as? VaultItemState.ViewState.Content,
|
||||
previousState = state.viewState.asContentOrNull(),
|
||||
isPremiumUser = account.isPremium,
|
||||
hasMasterPassword = account.hasMasterPassword,
|
||||
totpCodeItemData = this.data?.totpCodeItemData,
|
||||
|
@ -930,13 +880,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
when (val result = action.result) {
|
||||
ValidatePasswordResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is ValidatePasswordResult.Success -> {
|
||||
|
@ -953,13 +901,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
trySendAction(action.repromptAction.vaultItemAction)
|
||||
}
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -968,17 +914,15 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handleDeleteCipherReceive(action: VaultItemAction.Internal.DeleteCipherReceive) {
|
||||
when (action.result) {
|
||||
DeleteCipherResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
DeleteCipherResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
dismissDialog()
|
||||
sendEvent(
|
||||
VaultItemEvent.ShowToast(
|
||||
message = if (state.isCipherDeleted) {
|
||||
|
@ -996,17 +940,15 @@ class VaultItemViewModel @Inject constructor(
|
|||
private fun handleRestoreCipherReceive(action: VaultItemAction.Internal.RestoreCipherReceive) {
|
||||
when (action.result) {
|
||||
RestoreCipherResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
RestoreCipherResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
dismissDialog()
|
||||
sendEvent(VaultItemEvent.ShowToast(message = R.string.item_restored.asText()))
|
||||
sendEvent(VaultItemEvent.NavigateBack)
|
||||
}
|
||||
|
@ -1018,13 +960,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
) {
|
||||
when (val result = action.result) {
|
||||
DownloadAttachmentResult.Failure -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
message = R.string.unable_to_download_file.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = R.string.unable_to_download_file.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is DownloadAttachmentResult.Success -> {
|
||||
|
@ -1048,22 +988,44 @@ class VaultItemViewModel @Inject constructor(
|
|||
if (action.isSaved) {
|
||||
sendEvent(VaultItemEvent.ShowToast(R.string.save_attachment_success.asText()))
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
R.string.unable_to_save_attachment.asText(),
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
R.string.unable_to_save_attachment.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRestoreItemClicked() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.RestoreItemClick,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
updateDialogState(VaultItemState.DialogState.RestoreItemDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Internal Type Handlers
|
||||
|
||||
private fun updateDialogState(dialog: VaultItemState.DialogState?) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
updateDialogState(null)
|
||||
}
|
||||
|
||||
private inline fun onContent(
|
||||
crossinline block: (VaultItemState.ViewState.Content) -> Unit,
|
||||
) {
|
||||
(state.viewState as? VaultItemState.ViewState.Content)?.let(block)
|
||||
state.viewState.asContentOrNull()?.let(block)
|
||||
}
|
||||
|
||||
private inline fun onLoginContent(
|
||||
|
@ -1072,7 +1034,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
VaultItemState.ViewState.Content.ItemType.Login,
|
||||
) -> Unit,
|
||||
) {
|
||||
(state.viewState as? VaultItemState.ViewState.Content)
|
||||
state.viewState.asContentOrNull()
|
||||
?.let { content ->
|
||||
(content.type as? VaultItemState.ViewState.Content.ItemType.Login)
|
||||
?.let { loginContent ->
|
||||
|
@ -1087,7 +1049,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
VaultItemState.ViewState.Content.ItemType.Card,
|
||||
) -> Unit,
|
||||
) {
|
||||
(state.viewState as? VaultItemState.ViewState.Content)
|
||||
state.viewState.asContentOrNull()
|
||||
?.let { content ->
|
||||
(content.type as? VaultItemState.ViewState.Content.ItemType.Card)
|
||||
?.let { loginContent ->
|
||||
|
@ -1111,7 +1073,7 @@ data class VaultItemState(
|
|||
* Whether or not the cipher has been deleted.
|
||||
*/
|
||||
val isCipherDeleted: Boolean
|
||||
get() = (viewState as? ViewState.Content)
|
||||
get() = viewState.asContentOrNull()
|
||||
?.common
|
||||
?.currentCipher
|
||||
?.deletedDate != null
|
||||
|
@ -1126,7 +1088,7 @@ data class VaultItemState(
|
|||
* Whether or not the cipher is in a collection.
|
||||
*/
|
||||
val isCipherInCollection: Boolean
|
||||
get() = (viewState as? ViewState.Content)
|
||||
get() = viewState.asContentOrNull()
|
||||
?.common
|
||||
?.currentCipher
|
||||
?.collectionIds
|
||||
|
@ -1399,6 +1361,12 @@ data class VaultItemState(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to keep the syntax a little cleaner when safe casting specifically
|
||||
* for [Content]
|
||||
*/
|
||||
fun asContentOrNull(): Content? = this as? Content
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1445,6 +1413,12 @@ data class VaultItemState(
|
|||
data class Fido2CredentialCannotBeCopiedConfirmationPrompt(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays the dialog to prompt the user to confirm restoring a deleted item.
|
||||
*/
|
||||
@Parcelize
|
||||
data object RestoreItemDialog : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1541,6 +1515,12 @@ sealed class VaultItemAction {
|
|||
*/
|
||||
data object ConfirmDeleteClick : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked to restore a deleted item.
|
||||
|
||||
*/
|
||||
data object RestoreVaultItemClick : Common()
|
||||
|
||||
/**
|
||||
* The user has confirmed to restore the cipher.
|
||||
*/
|
||||
|
@ -1934,4 +1914,13 @@ sealed class PasswordRepromptAction : Parcelable {
|
|||
isVisible = this.isVisible,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should show the confirm restore
|
||||
*/
|
||||
@Parcelize
|
||||
data object RestoreItemClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.RestoreVaultItemClick
|
||||
}
|
||||
}
|
||||
|
|
|
@ -846,7 +846,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `Restore click should send show restore confirmation dialog`() {
|
||||
fun `Clicking Restore should send RestoreVaultItemClick ViewModel action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||
|
@ -867,6 +867,35 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.onNodeWithText("Restore")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.RestoreVaultItemClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Restore dialog should display correctly when dialog state changes`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||
.copy(
|
||||
common = DEFAULT_COMMON
|
||||
.copy(
|
||||
currentCipher = createMockCipherView(1).copy(
|
||||
deletedDate = Instant.MIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.RestoreItemDialog)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Do you really want to restore this item?")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
|
@ -889,7 +918,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `Restore dialog cancel click should hide restore confirmation menu`() {
|
||||
fun `Restore dialog should hide restore confirmation menu if dialog state changes`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||
|
@ -906,9 +935,9 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Restore")
|
||||
.performClick()
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.RestoreItemDialog)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Do you really want to restore this item?")
|
||||
|
@ -929,13 +958,16 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = null)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Restore dialog ok click should close the dialog and send ConfirmRestoreClick`() {
|
||||
fun `Restore dialog ok click should send ConfirmRestoreClick`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||
|
@ -952,24 +984,9 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Restore")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Do you really want to restore this item?")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Restore")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.RestoreItemDialog)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
|
@ -977,13 +994,44 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Common.ConfirmRestoreClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Restore dialog cancel click should send DismissDialogClick`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||
.copy(
|
||||
common = DEFAULT_COMMON
|
||||
.copy(
|
||||
currentCipher = createMockCipherView(1).copy(
|
||||
deletedDate = Instant.MIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.RestoreItemDialog)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Attachments option menu click should send AttachmentsClick action`() {
|
||||
// Confirm dropdown version of item is absent
|
||||
|
|
|
@ -396,6 +396,73 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
coVerify { vaultRepo.hardDeleteCipher(cipherId = VAULT_ITEM_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on RestoreItemClick should prompt for master password when required`() = runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
previousState = any(),
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem())
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.Common.RestoreVaultItemClick)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.RestoreItemClick,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on RestoreItemClick when no need to prompt for master password updates pendingCipher state correctly`() =
|
||||
runTest {
|
||||
val viewState =
|
||||
DEFAULT_VIEW_STATE.copy(common = DEFAULT_COMMON.copy(requiresReprompt = false))
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
previousState = any(),
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns viewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
val loginState = DEFAULT_STATE.copy(viewState = viewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
||||
// show dialog
|
||||
viewModel.trySendAction(VaultItemAction.Common.RestoreVaultItemClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.RestoreItemDialog),
|
||||
viewModel.stateFlow.value
|
||||
)
|
||||
|
||||
// dismiss dialog
|
||||
viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick)
|
||||
assertEquals(
|
||||
// setting this to be explicit.
|
||||
loginState.copy(dialog = null),
|
||||
viewModel.stateFlow.value
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `ConfirmRestoreClick with RestoreCipherResult Success should should ShowToast and NavigateBack`() =
|
||||
|
|
Loading…
Reference in a new issue