BIT-1646, BIT-1647: Launch action after password validation (#852)

This commit is contained in:
David Perez
2024-01-29 17:01:07 -06:00
committed by Álison Fernandes
parent 20dd839923
commit d12776483d
4 changed files with 592 additions and 140 deletions

View File

@@ -114,7 +114,14 @@ fun VaultItemScreen(
{ viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick) }
},
onSubmitMasterPassword = remember(viewModel) {
{ viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(it)) }
{ masterPassword, action ->
viewModel.trySendAction(
VaultItemAction.Common.MasterPasswordSubmit(
masterPassword = masterPassword,
action = action,
),
)
}
},
)
@@ -271,7 +278,7 @@ fun VaultItemScreen(
private fun VaultItemDialogs(
dialog: VaultItemState.DialogState?,
onDismissRequest: () -> Unit,
onSubmitMasterPassword: (String) -> Unit,
onSubmitMasterPassword: (masterPassword: String, action: PasswordRepromptAction) -> Unit,
) {
when (dialog) {
is VaultItemState.DialogState.Generic -> BitwardenBasicDialog(
@@ -286,9 +293,9 @@ private fun VaultItemDialogs(
visibilityState = LoadingDialogState.Shown(text = dialog.message),
)
VaultItemState.DialogState.MasterPasswordDialog -> {
is VaultItemState.DialogState.MasterPasswordDialog -> {
BitwardenMasterPasswordDialog(
onConfirmClick = onSubmitMasterPassword,
onConfirmClick = { onSubmitMasterPassword(it, dialog.action) },
onDismissRequest = onDismissRequest,
)
}

View File

@@ -40,7 +40,7 @@ private const val KEY_STATE = "state"
/**
* ViewModel responsible for handling user interactions in the vault item screen
*/
@Suppress("TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions")
@HiltViewModel
class VaultItemViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@@ -141,7 +141,11 @@ class VaultItemViewModel @Inject constructor(
onContent { content ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.EditClick,
),
)
}
return@onContent
}
@@ -160,7 +164,7 @@ class VaultItemViewModel @Inject constructor(
}
viewModelScope.launch {
val result = authRepository.validatePassword(action.masterPassword)
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result))
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result, action.action))
}
}
@@ -175,7 +179,11 @@ class VaultItemViewModel @Inject constructor(
onContent { content ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.CopyClick(action.field),
),
)
}
return@onContent
}
@@ -195,7 +203,14 @@ class VaultItemViewModel @Inject constructor(
onContent { content ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.ViewHiddenFieldClicked(
field = action.field,
isVisible = action.isVisible,
),
),
)
}
return@onContent
}
@@ -218,20 +233,51 @@ class VaultItemViewModel @Inject constructor(
}
private fun handleAttachmentsClick() {
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
onContent { content ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.AttachmentsClick,
),
)
}
return@onContent
}
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
}
}
private fun handleCloneClick() {
sendEvent(
VaultItemEvent.NavigateToAddEdit(
itemId = state.vaultItemId,
isClone = true,
),
)
onContent { content ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.CloneClick,
),
)
}
return@onContent
}
sendEvent(VaultItemEvent.NavigateToAddEdit(itemId = state.vaultItemId, isClone = true))
}
}
private fun handleMoveToOrganizationClick() {
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
onContent { content ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.MoveToOrganizationClick,
),
)
}
return@onContent
}
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
}
}
private fun handleCollectionsClick() {
@@ -239,29 +285,36 @@ class VaultItemViewModel @Inject constructor(
}
private fun handleConfirmDeleteClick() {
mutableStateFlow.update {
it.copy(
dialog = VaultItemState.DialogState.Loading(
R.string.soft_deleting.asText(),
),
)
}
onContent { content ->
content
.common
.currentCipher
?.let { cipher ->
viewModelScope.launch {
trySendAction(
VaultItemAction.Internal.DeleteCipherReceive(
result = vaultRepository.softDeleteCipher(
cipherId = state.vaultItemId,
cipherView = cipher,
),
),
)
}
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.DeleteClick,
),
)
}
return@onContent
}
mutableStateFlow.update {
it.copy(
dialog = VaultItemState.DialogState.Loading(
R.string.soft_deleting.asText(),
),
)
}
content.common.currentCipher?.let { cipher ->
viewModelScope.launch {
trySendAction(
VaultItemAction.Internal.DeleteCipherReceive(
result = vaultRepository.softDeleteCipher(
cipherId = state.vaultItemId,
cipherView = cipher,
),
),
)
}
}
}
}
@@ -347,13 +400,17 @@ class VaultItemViewModel @Inject constructor(
private fun handleCopyPasswordClick() {
onLoginContent { content, login ->
val password = requireNotNull(login.passwordData?.password)
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.CopyClick(value = password),
),
)
}
return@onLoginContent
}
val password = requireNotNull(login.passwordData?.password)
clipboardManager.setText(text = password)
}
}
@@ -370,13 +427,7 @@ class VaultItemViewModel @Inject constructor(
}
private fun handleCopyUsernameClick() {
onLoginContent { content, login ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
}
return@onLoginContent
}
onLoginContent { _, login ->
val username = requireNotNull(login.username)
clipboardManager.setText(text = username)
}
@@ -392,7 +443,11 @@ class VaultItemViewModel @Inject constructor(
onContent { content ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.PasswordHistoryClick,
),
)
}
return@onContent
}
@@ -406,7 +461,13 @@ class VaultItemViewModel @Inject constructor(
onLoginContent { content, login ->
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.ViewPasswordClick(
isVisible = action.isVisible,
),
),
)
}
return@onLoginContent
}
@@ -437,26 +498,34 @@ class VaultItemViewModel @Inject constructor(
private fun handleCopyNumberClick() {
onCardContent { content, card ->
val number = requireNotNull(card.number)
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.CopyClick(value = number),
),
)
}
return@onCardContent
}
val number = requireNotNull(card.number)
clipboardManager.setText(text = number)
}
}
private fun handleCopySecurityCodeClick() {
onCardContent { content, card ->
val securityCode = requireNotNull(card.securityCode)
if (content.common.requiresReprompt) {
mutableStateFlow.update {
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
it.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.CopyClick(value = securityCode),
),
)
}
return@onCardContent
}
val securityCode = requireNotNull(card.securityCode)
clipboardManager.setText(text = securityCode)
}
}
@@ -467,6 +536,7 @@ class VaultItemViewModel @Inject constructor(
private fun handleInternalAction(action: VaultItemAction.Internal) {
when (action) {
is VaultItemAction.Internal.CopyValue -> handleCopyValue(action)
is VaultItemAction.Internal.PasswordBreachReceive -> handlePasswordBreachReceive(action)
is VaultItemAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
is VaultItemAction.Internal.ValidatePasswordReceive -> handleValidatePasswordReceive(
@@ -478,6 +548,10 @@ class VaultItemViewModel @Inject constructor(
}
}
private fun handleCopyValue(action: VaultItemAction.Internal.CopyValue) {
clipboardManager.setText(action.value)
}
private fun handlePasswordBreachReceive(
action: VaultItemAction.Internal.PasswordBreachReceive,
) {
@@ -592,6 +666,7 @@ class VaultItemViewModel @Inject constructor(
),
)
}
trySendAction(action.repromptAction.vaultItemAction)
}
} else {
mutableStateFlow.update {
@@ -949,7 +1024,9 @@ data class VaultItemState(
* Displays the master password dialog to the user.
*/
@Parcelize
data object MasterPasswordDialog : DialogState()
data class MasterPasswordDialog(
val action: PasswordRepromptAction,
) : DialogState()
}
}
@@ -1054,6 +1131,7 @@ sealed class VaultItemAction {
*/
data class MasterPasswordSubmit(
val masterPassword: String,
val action: PasswordRepromptAction,
) : Common()
/**
@@ -1181,6 +1259,13 @@ sealed class VaultItemAction {
* Models actions that the [VaultItemViewModel] itself might send.
*/
sealed class Internal : VaultItemAction() {
/**
* Copies the given [value] to the clipboard.
*/
data class CopyValue(
val value: String,
) : Internal()
/**
* Indicates that the password breach results have been received.
*/
@@ -1201,6 +1286,7 @@ sealed class VaultItemAction {
*/
data class ValidatePasswordReceive(
val result: ValidatePasswordResult,
val repromptAction: PasswordRepromptAction,
) : Internal()
/**
@@ -1218,3 +1304,117 @@ sealed class VaultItemAction {
) : Internal()
}
}
/**
* Represents all the actions that can be taken after being prompted to a master password check.
*/
sealed class PasswordRepromptAction : Parcelable {
/**
* The Vault action that should be sent when password validation has completed.
*/
abstract val vaultItemAction: VaultItemAction
/**
* Indicates that we should launch the [VaultItemAction.Common.EditClick] upon password
* validation.
*/
@Parcelize
data object EditClick : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.Common.EditClick
}
/**
* Indicates that we should launch the [VaultItemAction.ItemType.Login.PasswordHistoryClick]
* upon password validation.
*/
@Parcelize
data object PasswordHistoryClick : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.ItemType.Login.PasswordHistoryClick
}
/**
* Indicates that we should launch the [VaultItemAction.Common.AttachmentsClick] upon password
* validation.
*/
@Parcelize
data object AttachmentsClick : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.Common.AttachmentsClick
}
/**
* Indicates that we should launch the [VaultItemAction.Common.CloneClick] upon password
* validation.
*/
@Parcelize
data object CloneClick : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.Common.CloneClick
}
/**
* Indicates that we should launch the [VaultItemAction.Common.MoveToOrganizationClick] upon
* password validation.
*/
@Parcelize
data object MoveToOrganizationClick : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.Common.MoveToOrganizationClick
}
/**
* Indicates that we should launch the [VaultItemAction.Common.ConfirmDeleteClick] upon
* password validation.
*/
@Parcelize
data object DeleteClick : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.Common.ConfirmDeleteClick
}
/**
* Indicates that we should launch the [VaultItemAction.Internal.CopyValue] upon password
* validation.
*/
@Parcelize
data class CopyClick(
val value: String,
) : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.Internal.CopyValue(
value = value,
)
}
/**
* Indicates that we should launch the
* [VaultItemAction.ItemType.Login.PasswordVisibilityClicked] upon password validation.
*/
@Parcelize
data class ViewPasswordClick(
val isVisible: Boolean,
) : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.ItemType.Login.PasswordVisibilityClicked(
isVisible = isVisible,
)
}
/**
* Indicates that we should launch the [VaultItemAction.Common.HiddenFieldVisibilityClicked]
* upon password validation.
*/
@Parcelize
data class ViewHiddenFieldClicked(
val field: VaultItemState.ViewState.Content.Common.Custom.HiddenField,
val isVisible: Boolean,
) : PasswordRepromptAction() {
override val vaultItemAction: VaultItemAction
get() = VaultItemAction.Common.HiddenFieldVisibilityClicked(
field = this.field,
isVisible = this.isVisible,
)
}
}