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.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
@ -69,11 +67,6 @@ fun VaultItemScreen(
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
val confirmRestoreAction = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(VaultItemAction.Common.ConfirmRestoreClick) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var pendingRestoreCipher by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val fileChooserLauncher = intentManager.getActivityResultLauncher { activityResult ->
|
val fileChooserLauncher = intentManager.getActivityResultLauncher { activityResult ->
|
||||||
intentManager.getFileDataFromActivityResult(activityResult)
|
intentManager.getFileDataFromActivityResult(activityResult)
|
||||||
|
@ -150,26 +143,12 @@ fun VaultItemScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
onConfirmRestoreAction = remember(viewModel) {
|
||||||
|
{
|
||||||
if (pendingRestoreCipher) {
|
viewModel.trySendAction(VaultItemAction.Common.ConfirmRestoreClick)
|
||||||
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())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
|
@ -189,7 +168,13 @@ fun VaultItemScreen(
|
||||||
if (state.isCipherDeleted) {
|
if (state.isCipherDeleted) {
|
||||||
BitwardenTextButton(
|
BitwardenTextButton(
|
||||||
label = stringResource(id = R.string.restore),
|
label = stringResource(id = R.string.restore),
|
||||||
onClick = { pendingRestoreCipher = true },
|
onClick = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.Common.RestoreVaultItemClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier.testTag("RestoreButton"),
|
modifier = Modifier.testTag("RestoreButton"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -298,6 +283,7 @@ private fun VaultItemDialogs(
|
||||||
onConfirmDeleteClick: () -> Unit,
|
onConfirmDeleteClick: () -> Unit,
|
||||||
onSubmitMasterPassword: (masterPassword: String, action: PasswordRepromptAction) -> Unit,
|
onSubmitMasterPassword: (masterPassword: String, action: PasswordRepromptAction) -> Unit,
|
||||||
onConfirmCloneWithoutFido2Credential: () -> Unit,
|
onConfirmCloneWithoutFido2Credential: () -> Unit,
|
||||||
|
onConfirmRestoreAction: () -> Unit,
|
||||||
) {
|
) {
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
is VaultItemState.DialogState.Generic -> BitwardenBasicDialog(
|
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
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,6 +165,8 @@ class VaultItemViewModel @Inject constructor(
|
||||||
is VaultItemAction.Common.ConfirmCloneWithoutFido2CredentialClick -> {
|
is VaultItemAction.Common.ConfirmCloneWithoutFido2CredentialClick -> {
|
||||||
handleConfirmCloneClick()
|
handleConfirmCloneClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultItemAction.Common.RestoreVaultItemClick -> handleRestoreItemClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,42 +175,36 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDismissDialogClick() {
|
private fun handleDismissDialogClick() {
|
||||||
mutableStateFlow.update { it.copy(dialog = null) }
|
dismissDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeleteClick() {
|
private fun handleDeleteClick() {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.DeleteClick,
|
action = PasswordRepromptAction.DeleteClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
} else {
|
} else {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState
|
||||||
dialog = VaultItemState
|
|
||||||
.DialogState
|
.DialogState
|
||||||
.DeleteConfirmationPrompt(state.deletionConfirmationText),
|
.DeleteConfirmationPrompt(state.deletionConfirmationText),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleEditClick() {
|
private fun handleEditClick() {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.EditClick,
|
action = PasswordRepromptAction.EditClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
sendEvent(
|
sendEvent(
|
||||||
|
@ -221,9 +217,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMasterPasswordSubmit(action: VaultItemAction.Common.MasterPasswordSubmit) {
|
private fun handleMasterPasswordSubmit(action: VaultItemAction.Common.MasterPasswordSubmit) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
||||||
it.copy(dialog = VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = authRepository.validatePassword(action.masterPassword)
|
val result = authRepository.validatePassword(action.masterPassword)
|
||||||
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result, action.action))
|
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result, action.action))
|
||||||
|
@ -240,13 +234,11 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.CopyClick(action.field),
|
action = PasswordRepromptAction.CopyClick(action.field),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
clipboardManager.setText(text = action.field)
|
clipboardManager.setText(text = action.field)
|
||||||
|
@ -269,16 +261,14 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.ViewHiddenFieldClicked(
|
action = PasswordRepromptAction.ViewHiddenFieldClicked(
|
||||||
field = action.field,
|
field = action.field,
|
||||||
isVisible = action.isVisible,
|
isVisible = action.isVisible,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
mutableStateFlow.update { currentState ->
|
mutableStateFlow.update { currentState ->
|
||||||
|
@ -311,23 +301,17 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.AttachmentDownloadClick(
|
action = PasswordRepromptAction.AttachmentDownloadClick(
|
||||||
attachment = action.attachment,
|
attachment = action.attachment,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableStateFlow.update {
|
updateDialogState(VaultItemState.DialogState.Loading(R.string.downloading.asText()))
|
||||||
it.copy(
|
|
||||||
dialog = VaultItemState.DialogState.Loading(R.string.downloading.asText()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = vaultRepository
|
val result = vaultRepository
|
||||||
|
@ -349,9 +333,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
private fun handleAttachmentFileLocationReceive(
|
private fun handleAttachmentFileLocationReceive(
|
||||||
action: VaultItemAction.Common.AttachmentFileLocationReceive,
|
action: VaultItemAction.Common.AttachmentFileLocationReceive,
|
||||||
) {
|
) {
|
||||||
mutableStateFlow.update {
|
dismissDialog()
|
||||||
it.copy(dialog = null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = temporaryAttachmentData ?: return
|
val file = temporaryAttachmentData ?: return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -374,25 +356,21 @@ class VaultItemViewModel @Inject constructor(
|
||||||
temporaryAttachmentData?.let { fileManager.delete(it) }
|
temporaryAttachmentData?.let { fileManager.delete(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Generic(
|
||||||
dialog = VaultItemState.DialogState.Generic(
|
|
||||||
R.string.unable_to_save_attachment.asText(),
|
R.string.unable_to_save_attachment.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleAttachmentsClick() {
|
private fun handleAttachmentsClick() {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.AttachmentsClick,
|
action = PasswordRepromptAction.AttachmentsClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
|
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
|
||||||
|
@ -403,22 +381,18 @@ class VaultItemViewModel @Inject constructor(
|
||||||
private fun handleCloneClick() {
|
private fun handleCloneClick() {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresCloneConfirmation) {
|
if (content.common.requiresCloneConfirmation) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Fido2CredentialCannotBeCopiedConfirmationPrompt(
|
||||||
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(),
|
message = R.string.the_passkey_will_not_be_copied_to_the_cloned_item_do_you_want_to_continue_cloning_this_item.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
} else if (content.common.requiresReprompt) {
|
} else if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.CloneClick,
|
action = PasswordRepromptAction.CloneClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
sendEvent(VaultItemEvent.NavigateToAddEdit(itemId = state.vaultItemId, isClone = true))
|
sendEvent(VaultItemEvent.NavigateToAddEdit(itemId = state.vaultItemId, isClone = true))
|
||||||
|
@ -445,13 +419,11 @@ class VaultItemViewModel @Inject constructor(
|
||||||
private fun handleMoveToOrganizationClick() {
|
private fun handleMoveToOrganizationClick() {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.MoveToOrganizationClick,
|
action = PasswordRepromptAction.MoveToOrganizationClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
|
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
|
||||||
|
@ -464,9 +436,8 @@ class VaultItemViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun handleConfirmDeleteClick() {
|
private fun handleConfirmDeleteClick() {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Loading(
|
||||||
dialog = VaultItemState.DialogState.Loading(
|
|
||||||
if (state.isCipherDeleted) {
|
if (state.isCipherDeleted) {
|
||||||
R.string.deleting.asText()
|
R.string.deleting.asText()
|
||||||
} else {
|
} else {
|
||||||
|
@ -474,7 +445,6 @@ class VaultItemViewModel @Inject constructor(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
content.common.currentCipher?.let { cipher ->
|
content.common.currentCipher?.let { cipher ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
trySendAction(
|
trySendAction(
|
||||||
|
@ -497,13 +467,11 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleConfirmRestoreClick() {
|
private fun handleConfirmRestoreClick() {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Loading(
|
||||||
dialog = VaultItemState.DialogState.Loading(
|
|
||||||
R.string.restoring.asText(),
|
R.string.restoring.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
content
|
content
|
||||||
.common
|
.common
|
||||||
|
@ -566,9 +534,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
private fun handleCheckForBreachClick() {
|
private fun handleCheckForBreachClick() {
|
||||||
onLoginContent { _, login ->
|
onLoginContent { _, login ->
|
||||||
val password = requireNotNull(login.passwordData?.password)
|
val password = requireNotNull(login.passwordData?.password)
|
||||||
mutableStateFlow.update {
|
updateDialogState(VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
||||||
it.copy(dialog = VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = authRepository.getPasswordBreachCount(password = password)
|
val result = authRepository.getPasswordBreachCount(password = password)
|
||||||
sendAction(VaultItemAction.Internal.PasswordBreachReceive(result))
|
sendAction(VaultItemAction.Internal.PasswordBreachReceive(result))
|
||||||
|
@ -580,13 +546,11 @@ class VaultItemViewModel @Inject constructor(
|
||||||
onLoginContent { content, login ->
|
onLoginContent { content, login ->
|
||||||
val password = requireNotNull(login.passwordData?.password)
|
val password = requireNotNull(login.passwordData?.password)
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.CopyClick(value = password),
|
action = PasswordRepromptAction.CopyClick(value = password),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onLoginContent
|
return@onLoginContent
|
||||||
}
|
}
|
||||||
clipboardManager.setText(text = password)
|
clipboardManager.setText(text = password)
|
||||||
|
@ -623,13 +587,11 @@ class VaultItemViewModel @Inject constructor(
|
||||||
private fun handlePasswordHistoryClick() {
|
private fun handlePasswordHistoryClick() {
|
||||||
onContent { content ->
|
onContent { content ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.PasswordHistoryClick,
|
action = PasswordRepromptAction.PasswordHistoryClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onContent
|
return@onContent
|
||||||
}
|
}
|
||||||
sendEvent(VaultItemEvent.NavigateToPasswordHistory(state.vaultItemId))
|
sendEvent(VaultItemEvent.NavigateToPasswordHistory(state.vaultItemId))
|
||||||
|
@ -641,15 +603,13 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
onLoginContent { content, login ->
|
onLoginContent { content, login ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.ViewPasswordClick(
|
action = PasswordRepromptAction.ViewPasswordClick(
|
||||||
isVisible = action.isVisible,
|
isVisible = action.isVisible,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onLoginContent
|
return@onLoginContent
|
||||||
}
|
}
|
||||||
mutableStateFlow.update { currentState ->
|
mutableStateFlow.update { currentState ->
|
||||||
|
@ -696,15 +656,13 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
onCardContent { content, card ->
|
onCardContent { content, card ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.ViewCodeClick(
|
action = PasswordRepromptAction.ViewCodeClick(
|
||||||
isVisible = action.isVisible,
|
isVisible = action.isVisible,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onCardContent
|
return@onCardContent
|
||||||
}
|
}
|
||||||
mutableStateFlow.update { currentState ->
|
mutableStateFlow.update { currentState ->
|
||||||
|
@ -732,13 +690,11 @@ class VaultItemViewModel @Inject constructor(
|
||||||
onCardContent { content, card ->
|
onCardContent { content, card ->
|
||||||
val number = requireNotNull(card.number).number
|
val number = requireNotNull(card.number).number
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.CopyClick(value = number),
|
action = PasswordRepromptAction.CopyClick(value = number),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onCardContent
|
return@onCardContent
|
||||||
}
|
}
|
||||||
clipboardManager.setText(text = number)
|
clipboardManager.setText(text = number)
|
||||||
|
@ -749,13 +705,11 @@ class VaultItemViewModel @Inject constructor(
|
||||||
onCardContent { content, card ->
|
onCardContent { content, card ->
|
||||||
val securityCode = requireNotNull(card.securityCode).code
|
val securityCode = requireNotNull(card.securityCode).code
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.CopyClick(value = securityCode),
|
action = PasswordRepromptAction.CopyClick(value = securityCode),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onCardContent
|
return@onCardContent
|
||||||
}
|
}
|
||||||
clipboardManager.setText(text = securityCode)
|
clipboardManager.setText(text = securityCode)
|
||||||
|
@ -767,15 +721,13 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
onCardContent { content, card ->
|
onCardContent { content, card ->
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
|
||||||
action = PasswordRepromptAction.ViewNumberClick(
|
action = PasswordRepromptAction.ViewNumberClick(
|
||||||
isVisible = action.isVisible,
|
isVisible = action.isVisible,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return@onCardContent
|
return@onCardContent
|
||||||
}
|
}
|
||||||
mutableStateFlow.update { currentState ->
|
mutableStateFlow.update { currentState ->
|
||||||
|
@ -842,9 +794,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutableStateFlow.update {
|
updateDialogState(VaultItemState.DialogState.Generic(message = message))
|
||||||
it.copy(dialog = VaultItemState.DialogState.Generic(message = message))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
|
@ -918,7 +868,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
.data
|
.data
|
||||||
?.cipher
|
?.cipher
|
||||||
?.toViewState(
|
?.toViewState(
|
||||||
previousState = state.viewState as? VaultItemState.ViewState.Content,
|
previousState = state.viewState.asContentOrNull(),
|
||||||
isPremiumUser = account.isPremium,
|
isPremiumUser = account.isPremium,
|
||||||
hasMasterPassword = account.hasMasterPassword,
|
hasMasterPassword = account.hasMasterPassword,
|
||||||
totpCodeItemData = this.data?.totpCodeItemData,
|
totpCodeItemData = this.data?.totpCodeItemData,
|
||||||
|
@ -930,14 +880,12 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
when (val result = action.result) {
|
when (val result = action.result) {
|
||||||
ValidatePasswordResult.Error -> {
|
ValidatePasswordResult.Error -> {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Generic(
|
||||||
dialog = VaultItemState.DialogState.Generic(
|
|
||||||
message = R.string.generic_error_message.asText(),
|
message = R.string.generic_error_message.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is ValidatePasswordResult.Success -> {
|
is ValidatePasswordResult.Success -> {
|
||||||
if (result.isValid) {
|
if (result.isValid) {
|
||||||
|
@ -953,9 +901,8 @@ class VaultItemViewModel @Inject constructor(
|
||||||
trySendAction(action.repromptAction.vaultItemAction)
|
trySendAction(action.repromptAction.vaultItemAction)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Generic(
|
||||||
dialog = VaultItemState.DialogState.Generic(
|
|
||||||
message = R.string.invalid_master_password.asText(),
|
message = R.string.invalid_master_password.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -963,22 +910,19 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDeleteCipherReceive(action: VaultItemAction.Internal.DeleteCipherReceive) {
|
private fun handleDeleteCipherReceive(action: VaultItemAction.Internal.DeleteCipherReceive) {
|
||||||
when (action.result) {
|
when (action.result) {
|
||||||
DeleteCipherResult.Error -> {
|
DeleteCipherResult.Error -> {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Generic(
|
||||||
dialog = VaultItemState.DialogState.Generic(
|
|
||||||
message = R.string.generic_error_message.asText(),
|
message = R.string.generic_error_message.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
DeleteCipherResult.Success -> {
|
DeleteCipherResult.Success -> {
|
||||||
mutableStateFlow.update { it.copy(dialog = null) }
|
dismissDialog()
|
||||||
sendEvent(
|
sendEvent(
|
||||||
VaultItemEvent.ShowToast(
|
VaultItemEvent.ShowToast(
|
||||||
message = if (state.isCipherDeleted) {
|
message = if (state.isCipherDeleted) {
|
||||||
|
@ -996,17 +940,15 @@ class VaultItemViewModel @Inject constructor(
|
||||||
private fun handleRestoreCipherReceive(action: VaultItemAction.Internal.RestoreCipherReceive) {
|
private fun handleRestoreCipherReceive(action: VaultItemAction.Internal.RestoreCipherReceive) {
|
||||||
when (action.result) {
|
when (action.result) {
|
||||||
RestoreCipherResult.Error -> {
|
RestoreCipherResult.Error -> {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Generic(
|
||||||
dialog = VaultItemState.DialogState.Generic(
|
|
||||||
message = R.string.generic_error_message.asText(),
|
message = R.string.generic_error_message.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
RestoreCipherResult.Success -> {
|
RestoreCipherResult.Success -> {
|
||||||
mutableStateFlow.update { it.copy(dialog = null) }
|
dismissDialog()
|
||||||
sendEvent(VaultItemEvent.ShowToast(message = R.string.item_restored.asText()))
|
sendEvent(VaultItemEvent.ShowToast(message = R.string.item_restored.asText()))
|
||||||
sendEvent(VaultItemEvent.NavigateBack)
|
sendEvent(VaultItemEvent.NavigateBack)
|
||||||
}
|
}
|
||||||
|
@ -1018,14 +960,12 @@ class VaultItemViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
when (val result = action.result) {
|
when (val result = action.result) {
|
||||||
DownloadAttachmentResult.Failure -> {
|
DownloadAttachmentResult.Failure -> {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Generic(
|
||||||
dialog = VaultItemState.DialogState.Generic(
|
|
||||||
message = R.string.unable_to_download_file.asText(),
|
message = R.string.unable_to_download_file.asText(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is DownloadAttachmentResult.Success -> {
|
is DownloadAttachmentResult.Success -> {
|
||||||
temporaryAttachmentData = result.file
|
temporaryAttachmentData = result.file
|
||||||
|
@ -1048,22 +988,44 @@ class VaultItemViewModel @Inject constructor(
|
||||||
if (action.isSaved) {
|
if (action.isSaved) {
|
||||||
sendEvent(VaultItemEvent.ShowToast(R.string.save_attachment_success.asText()))
|
sendEvent(VaultItemEvent.ShowToast(R.string.save_attachment_success.asText()))
|
||||||
} else {
|
} else {
|
||||||
mutableStateFlow.update {
|
updateDialogState(
|
||||||
it.copy(
|
VaultItemState.DialogState.Generic(
|
||||||
dialog = VaultItemState.DialogState.Generic(
|
|
||||||
R.string.unable_to_save_attachment.asText(),
|
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
|
//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(
|
private inline fun onContent(
|
||||||
crossinline block: (VaultItemState.ViewState.Content) -> Unit,
|
crossinline block: (VaultItemState.ViewState.Content) -> Unit,
|
||||||
) {
|
) {
|
||||||
(state.viewState as? VaultItemState.ViewState.Content)?.let(block)
|
state.viewState.asContentOrNull()?.let(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun onLoginContent(
|
private inline fun onLoginContent(
|
||||||
|
@ -1072,7 +1034,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
VaultItemState.ViewState.Content.ItemType.Login,
|
VaultItemState.ViewState.Content.ItemType.Login,
|
||||||
) -> Unit,
|
) -> Unit,
|
||||||
) {
|
) {
|
||||||
(state.viewState as? VaultItemState.ViewState.Content)
|
state.viewState.asContentOrNull()
|
||||||
?.let { content ->
|
?.let { content ->
|
||||||
(content.type as? VaultItemState.ViewState.Content.ItemType.Login)
|
(content.type as? VaultItemState.ViewState.Content.ItemType.Login)
|
||||||
?.let { loginContent ->
|
?.let { loginContent ->
|
||||||
|
@ -1087,7 +1049,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
VaultItemState.ViewState.Content.ItemType.Card,
|
VaultItemState.ViewState.Content.ItemType.Card,
|
||||||
) -> Unit,
|
) -> Unit,
|
||||||
) {
|
) {
|
||||||
(state.viewState as? VaultItemState.ViewState.Content)
|
state.viewState.asContentOrNull()
|
||||||
?.let { content ->
|
?.let { content ->
|
||||||
(content.type as? VaultItemState.ViewState.Content.ItemType.Card)
|
(content.type as? VaultItemState.ViewState.Content.ItemType.Card)
|
||||||
?.let { loginContent ->
|
?.let { loginContent ->
|
||||||
|
@ -1111,7 +1073,7 @@ data class VaultItemState(
|
||||||
* Whether or not the cipher has been deleted.
|
* Whether or not the cipher has been deleted.
|
||||||
*/
|
*/
|
||||||
val isCipherDeleted: Boolean
|
val isCipherDeleted: Boolean
|
||||||
get() = (viewState as? ViewState.Content)
|
get() = viewState.asContentOrNull()
|
||||||
?.common
|
?.common
|
||||||
?.currentCipher
|
?.currentCipher
|
||||||
?.deletedDate != null
|
?.deletedDate != null
|
||||||
|
@ -1126,7 +1088,7 @@ data class VaultItemState(
|
||||||
* Whether or not the cipher is in a collection.
|
* Whether or not the cipher is in a collection.
|
||||||
*/
|
*/
|
||||||
val isCipherInCollection: Boolean
|
val isCipherInCollection: Boolean
|
||||||
get() = (viewState as? ViewState.Content)
|
get() = viewState.asContentOrNull()
|
||||||
?.common
|
?.common
|
||||||
?.currentCipher
|
?.currentCipher
|
||||||
?.collectionIds
|
?.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(
|
data class Fido2CredentialCannotBeCopiedConfirmationPrompt(
|
||||||
val message: Text,
|
val message: Text,
|
||||||
) : DialogState()
|
) : 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()
|
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.
|
* The user has confirmed to restore the cipher.
|
||||||
*/
|
*/
|
||||||
|
@ -1934,4 +1914,13 @@ sealed class PasswordRepromptAction : Parcelable {
|
||||||
isVisible = this.isVisible,
|
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
|
@Test
|
||||||
fun `Restore click should send show restore confirmation dialog`() {
|
fun `Clicking Restore should send RestoreVaultItemClick ViewModel action`() {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||||
|
@ -867,6 +867,35 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
.onNodeWithText("Restore")
|
.onNodeWithText("Restore")
|
||||||
.performClick()
|
.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
|
composeTestRule
|
||||||
.onAllNodesWithText("Do you really want to restore this item?")
|
.onAllNodesWithText("Do you really want to restore this item?")
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
@ -889,7 +918,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||||
|
@ -906,9 +935,9 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
composeTestRule.assertNoDialogExists()
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
composeTestRule
|
mutableStateFlow.update {
|
||||||
.onNodeWithText("Restore")
|
it.copy(dialog = VaultItemState.DialogState.RestoreItemDialog)
|
||||||
.performClick()
|
}
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onAllNodesWithText("Do you really want to restore this item?")
|
.onAllNodesWithText("Do you really want to restore this item?")
|
||||||
|
@ -929,13 +958,16 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
.onAllNodesWithText("Cancel")
|
.onAllNodesWithText("Cancel")
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
.performClick()
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialog = null)
|
||||||
|
}
|
||||||
|
|
||||||
composeTestRule.assertNoDialogExists()
|
composeTestRule.assertNoDialogExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Restore dialog ok click should close the dialog and send ConfirmRestoreClick`() {
|
fun `Restore dialog ok click should send ConfirmRestoreClick`() {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
viewState = DEFAULT_IDENTITY_VIEW_STATE
|
||||||
|
@ -952,24 +984,9 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
composeTestRule.assertNoDialogExists()
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
composeTestRule
|
mutableStateFlow.update {
|
||||||
.onNodeWithText("Restore")
|
it.copy(dialog = VaultItemState.DialogState.RestoreItemDialog)
|
||||||
.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()
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onAllNodesWithText("Ok")
|
.onAllNodesWithText("Ok")
|
||||||
|
@ -977,13 +994,44 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
composeTestRule.assertNoDialogExists()
|
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
viewModel.trySendAction(VaultItemAction.Common.ConfirmRestoreClick)
|
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
|
@Test
|
||||||
fun `Attachments option menu click should send AttachmentsClick action`() {
|
fun `Attachments option menu click should send AttachmentsClick action`() {
|
||||||
// Confirm dropdown version of item is absent
|
// Confirm dropdown version of item is absent
|
||||||
|
|
|
@ -396,6 +396,73 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
coVerify { vaultRepo.hardDeleteCipher(cipherId = VAULT_ITEM_ID) }
|
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
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `ConfirmRestoreClick with RestoreCipherResult Success should should ShowToast and NavigateBack`() =
|
fun `ConfirmRestoreClick with RestoreCipherResult Success should should ShowToast and NavigateBack`() =
|
||||||
|
|
Loading…
Reference in a new issue