mirror of
https://github.com/bitwarden/android.git
synced 2026-10-23 03:23:53 -05:00
PM-10286: View Master Password Prompt (#5280)
This commit is contained in:
1 parent
997769bb1c
commit
f43367ebfa
36 files changed
+1779
-2636
No files matched your search
@@ -121,10 +121,13 @@ fun SearchContent(
|
||||
supportingLabelTestTag = it.subtitleTestTag,
|
||||
optionsTestTag = it.overflowTestTag,
|
||||
onClick = {
|
||||
if (it.isTotp && it.shouldDisplayMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(it.id)
|
||||
} else if (it.autofillSelectionOptions.isNotEmpty()) {
|
||||
if (it.autofillSelectionOptions.isNotEmpty()) {
|
||||
autofillSelectionOptionsItem = it
|
||||
} else if (it.shouldDisplayMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.ViewItem(
|
||||
cipherId = it.id,
|
||||
itemType = it.itemType,
|
||||
)
|
||||
} else {
|
||||
searchHandlers.onItemClick(it.id, it.itemType)
|
||||
}
|
||||
|
||||
+11
-12
@@ -643,11 +643,11 @@ class SearchViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
is MasterPasswordRepromptData.Totp -> {
|
||||
is MasterPasswordRepromptData.ViewItem -> {
|
||||
trySendAction(
|
||||
action = SearchAction.ItemClick(
|
||||
itemId = data.cipherId,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(type = CipherType.LOGIN),
|
||||
itemType = data.itemType,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -761,7 +761,6 @@ class SearchViewModel @Inject constructor(
|
||||
baseIconUrl = state.baseIconUrl,
|
||||
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
||||
isAutofill = state.isAutofill,
|
||||
isTotp = state.isTotp,
|
||||
isPremiumUser = state.isPremium,
|
||||
)
|
||||
}
|
||||
@@ -909,7 +908,6 @@ data class SearchState(
|
||||
val overflowOptions: List<ListingItemOverflowAction>,
|
||||
val overflowTestTag: String?,
|
||||
val autofillSelectionOptions: List<AutofillSelectionOption>,
|
||||
val isTotp: Boolean,
|
||||
val shouldDisplayMasterPasswordReprompt: Boolean,
|
||||
val itemType: ItemType,
|
||||
) : Parcelable {
|
||||
@@ -1296,14 +1294,6 @@ sealed class MasterPasswordRepromptData : Parcelable {
|
||||
val cipherId: String,
|
||||
) : MasterPasswordRepromptData()
|
||||
|
||||
/**
|
||||
* Autofill was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Totp(
|
||||
val cipherId: String,
|
||||
) : MasterPasswordRepromptData()
|
||||
|
||||
/**
|
||||
* A cipher overflow menu item action was selected.
|
||||
*/
|
||||
@@ -1311,4 +1301,13 @@ sealed class MasterPasswordRepromptData : Parcelable {
|
||||
data class OverflowItem(
|
||||
val action: ListingItemOverflowAction.VaultAction,
|
||||
) : MasterPasswordRepromptData()
|
||||
|
||||
/**
|
||||
* Item was selected to be viewed.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewItem(
|
||||
val cipherId: String,
|
||||
val itemType: SearchState.DisplayItem.ItemType,
|
||||
) : MasterPasswordRepromptData()
|
||||
}
|
||||
+2
-10
@@ -151,7 +151,6 @@ fun List<CipherView>.toViewState(
|
||||
hasMasterPassword: Boolean,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
isAutofill: Boolean,
|
||||
isTotp: Boolean,
|
||||
isPremiumUser: Boolean,
|
||||
): SearchState.ViewState =
|
||||
when {
|
||||
@@ -163,7 +162,6 @@ fun List<CipherView>.toViewState(
|
||||
hasMasterPassword = hasMasterPassword,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
isAutofill = isAutofill,
|
||||
isTotp = isTotp,
|
||||
isPremiumUser = isPremiumUser,
|
||||
)
|
||||
.sortAlphabetically(),
|
||||
@@ -177,13 +175,11 @@ fun List<CipherView>.toViewState(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun List<CipherView>.toDisplayItemList(
|
||||
baseIconUrl: String,
|
||||
hasMasterPassword: Boolean,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
isAutofill: Boolean,
|
||||
isTotp: Boolean,
|
||||
isPremiumUser: Boolean,
|
||||
): List<SearchState.DisplayItem> =
|
||||
this.map {
|
||||
@@ -192,18 +188,15 @@ private fun List<CipherView>.toDisplayItemList(
|
||||
hasMasterPassword = hasMasterPassword,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
isAutofill = isAutofill,
|
||||
isTotp = isTotp,
|
||||
isPremiumUser = isPremiumUser,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun CipherView.toDisplayItem(
|
||||
baseIconUrl: String,
|
||||
hasMasterPassword: Boolean,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
isAutofill: Boolean,
|
||||
isTotp: Boolean,
|
||||
isPremiumUser: Boolean,
|
||||
): SearchState.DisplayItem =
|
||||
SearchState.DisplayItem(
|
||||
@@ -231,8 +224,8 @@ private fun CipherView.toDisplayItem(
|
||||
.filter {
|
||||
this.login != null || (it != AutofillSelectionOption.AUTOFILL_AND_SAVE)
|
||||
},
|
||||
isTotp = isTotp,
|
||||
shouldDisplayMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
shouldDisplayMasterPasswordReprompt = hasMasterPassword &&
|
||||
reprompt == CipherRepromptType.PASSWORD,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(type = this.type),
|
||||
)
|
||||
|
||||
@@ -373,7 +366,6 @@ private fun SendView.toDisplayItem(
|
||||
overflowTestTag = "SendOptionsButton",
|
||||
totpCode = null,
|
||||
autofillSelectionOptions = emptyList(),
|
||||
isTotp = false,
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
itemType = SearchState.DisplayItem.ItemType.Sends(type = this.type),
|
||||
)
|
||||
|
||||
@@ -34,7 +34,6 @@ import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
@@ -127,16 +126,6 @@ fun VaultItemScreen(
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick) }
|
||||
},
|
||||
onSubmitMasterPassword = remember(viewModel) {
|
||||
{ masterPassword, action ->
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.MasterPasswordSubmit(
|
||||
masterPassword = masterPassword,
|
||||
action = action,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
onConfirmDeleteClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.ConfirmDeleteClick) }
|
||||
},
|
||||
@@ -288,7 +277,6 @@ private fun VaultItemDialogs(
|
||||
dialog: VaultItemState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirmDeleteClick: () -> Unit,
|
||||
onSubmitMasterPassword: (masterPassword: String, action: PasswordRepromptAction) -> Unit,
|
||||
onConfirmCloneWithoutFido2Credential: () -> Unit,
|
||||
onConfirmRestoreAction: () -> Unit,
|
||||
) {
|
||||
@@ -304,13 +292,6 @@ private fun VaultItemDialogs(
|
||||
text = dialog.message(),
|
||||
)
|
||||
|
||||
is VaultItemState.DialogState.MasterPasswordDialog -> {
|
||||
BitwardenMasterPasswordDialog(
|
||||
onConfirmClick = { onSubmitMasterPassword(it, dialog.action) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemState.DialogState.DeleteConfirmationPrompt -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.delete),
|
||||
|
||||
+20
-445
@@ -17,7 +17,6 @@ import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
@@ -234,7 +233,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
is VaultItemAction.Common.CloseClick -> handleCloseClick()
|
||||
is VaultItemAction.Common.DismissDialogClick -> handleDismissDialogClick()
|
||||
is VaultItemAction.Common.EditClick -> handleEditClick()
|
||||
is VaultItemAction.Common.MasterPasswordSubmit -> handleMasterPasswordSubmit(action)
|
||||
is VaultItemAction.Common.RefreshClick -> handleRefreshClick()
|
||||
is VaultItemAction.Common.CopyCustomHiddenFieldClick -> {
|
||||
handleCopyCustomHiddenFieldClick(action)
|
||||
@@ -286,50 +284,21 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleDeleteClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
} else {
|
||||
updateDialogState(
|
||||
VaultItemState
|
||||
.DialogState
|
||||
.DeleteConfirmationPrompt(state.deletionConfirmationText),
|
||||
)
|
||||
}
|
||||
}
|
||||
updateDialogState(
|
||||
dialog = VaultItemState.DialogState.DeleteConfirmationPrompt(
|
||||
message = state.deletionConfirmationText,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleEditClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.EditClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(
|
||||
VaultItemEvent.NavigateToAddEdit(
|
||||
itemId = state.vaultItemId,
|
||||
isClone = false,
|
||||
type = state.cipherType,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordSubmit(action: VaultItemAction.Common.MasterPasswordSubmit) {
|
||||
updateDialogState(VaultItemState.DialogState.Loading(R.string.loading.asText()))
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.validatePassword(action.masterPassword)
|
||||
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result, action.action))
|
||||
}
|
||||
sendEvent(
|
||||
VaultItemEvent.NavigateToAddEdit(
|
||||
itemId = state.vaultItemId,
|
||||
isClone = false,
|
||||
type = state.cipherType,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleRefreshClick() {
|
||||
@@ -341,14 +310,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
action: VaultItemAction.Common.CopyCustomHiddenFieldClick,
|
||||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(action.field),
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
clipboardManager.setText(text = action.field)
|
||||
organizationEventManager.trackEvent(
|
||||
event = OrganizationEvent.CipherClientCopiedHiddenField(
|
||||
@@ -368,17 +329,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
action: VaultItemAction.Common.HiddenFieldVisibilityClicked,
|
||||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewHiddenFieldClicked(
|
||||
field = action.field,
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = content.copy(
|
||||
@@ -408,17 +358,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
action: VaultItemAction.Common.AttachmentDownloadClick,
|
||||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentDownloadClick(
|
||||
attachment = action.attachment,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
|
||||
updateDialogState(VaultItemState.DialogState.Loading(R.string.downloading.asText()))
|
||||
|
||||
viewModelScope.launch {
|
||||
@@ -472,36 +411,19 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleAttachmentsClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentsClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun handleCloneClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresCloneConfirmation) {
|
||||
updateDialogState(
|
||||
@Suppress("MaxLineLength")
|
||||
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) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CloneClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(
|
||||
event = VaultItemEvent.NavigateToAddEdit(
|
||||
@@ -531,17 +453,7 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleMoveToOrganizationClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.MoveToOrganizationClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
|
||||
}
|
||||
|
||||
private fun handleCollectionsClick() {
|
||||
@@ -676,17 +588,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
|
||||
private fun handleCopyPasswordClick() {
|
||||
onLoginContent { content, login ->
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = password),
|
||||
),
|
||||
)
|
||||
return@onLoginContent
|
||||
}
|
||||
clipboardManager.setText(
|
||||
text = password,
|
||||
text = requireNotNull(login.passwordData).password,
|
||||
toastDescriptorOverride = R.string.password.asText(),
|
||||
)
|
||||
organizationEventManager.trackEvent(
|
||||
@@ -729,33 +632,13 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handlePasswordHistoryClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.PasswordHistoryClick,
|
||||
),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToPasswordHistory(state.vaultItemId))
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToPasswordHistory(itemId = state.vaultItemId))
|
||||
}
|
||||
|
||||
private fun handlePasswordVisibilityClicked(
|
||||
action: VaultItemAction.ItemType.Login.PasswordVisibilityClicked,
|
||||
) {
|
||||
onLoginContent { content, login ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewPasswordClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@onLoginContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = content.copy(
|
||||
@@ -799,16 +682,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
action: VaultItemAction.ItemType.Card.CodeVisibilityClick,
|
||||
) {
|
||||
onCardContent { content, card ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewCodeClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = content.copy(
|
||||
@@ -832,17 +705,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
|
||||
private fun handleCopyNumberClick() {
|
||||
onCardContent { content, card ->
|
||||
val number = requireNotNull(card.number).number
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = number),
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
clipboardManager.setText(
|
||||
text = number,
|
||||
text = requireNotNull(card.number).number,
|
||||
toastDescriptorOverride = R.string.number.asText(),
|
||||
)
|
||||
}
|
||||
@@ -850,17 +714,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
|
||||
private fun handleCopySecurityCodeClick() {
|
||||
onCardContent { content, card ->
|
||||
val securityCode = requireNotNull(card.securityCode).code
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = securityCode),
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
clipboardManager.setText(
|
||||
text = securityCode,
|
||||
text = requireNotNull(card.securityCode).code,
|
||||
toastDescriptorOverride = R.string.security_code.asText(),
|
||||
)
|
||||
}
|
||||
@@ -870,16 +725,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
action: VaultItemAction.ItemType.Card.NumberVisibilityClick,
|
||||
) {
|
||||
onCardContent { content, card ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewNumberClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@onCardContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = content.copy(
|
||||
@@ -932,16 +777,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
action: VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked,
|
||||
) {
|
||||
onSshKeyContent { content, sshKey ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewPrivateKeyClicked(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@onSshKeyContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = content.copy(
|
||||
@@ -954,14 +789,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
|
||||
private fun handleCopyPrivateKeyClick() {
|
||||
onSshKeyContent { content, sshKey ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = sshKey.privateKey),
|
||||
),
|
||||
)
|
||||
return@onSshKeyContent
|
||||
}
|
||||
clipboardManager.setText(
|
||||
text = sshKey.privateKey,
|
||||
toastDescriptorOverride = R.string.private_key.asText(),
|
||||
@@ -1107,13 +934,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
is VaultItemAction.Internal.CopyValue -> handleCopyValue(action)
|
||||
is VaultItemAction.Internal.PasswordBreachReceive -> handlePasswordBreachReceive(action)
|
||||
is VaultItemAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
is VaultItemAction.Internal.ValidatePasswordReceive -> handleValidatePasswordReceive(
|
||||
action,
|
||||
)
|
||||
|
||||
is VaultItemAction.Internal.DeleteCipherReceive -> handleDeleteCipherReceive(action)
|
||||
is VaultItemAction.Internal.RestoreCipherReceive -> handleRestoreCipherReceive(action)
|
||||
|
||||
is VaultItemAction.Internal.AttachmentDecryptReceive -> {
|
||||
handleAttachmentDecryptReceive(action)
|
||||
}
|
||||
@@ -1229,7 +1051,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
?.toViewState(
|
||||
previousState = state.viewState.asContentOrNull(),
|
||||
isPremiumUser = account.isPremium,
|
||||
hasMasterPassword = account.hasMasterPassword,
|
||||
totpCodeItemData = this.data?.totpCodeItemData,
|
||||
canDelete = this.data?.canDelete == true,
|
||||
canRestore = this.data?.canRestore == true,
|
||||
@@ -1241,43 +1062,6 @@ class VaultItemViewModel @Inject constructor(
|
||||
)
|
||||
?: VaultItemState.ViewState.Error(message = errorText)
|
||||
|
||||
private fun handleValidatePasswordReceive(
|
||||
action: VaultItemAction.Internal.ValidatePasswordReceive,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is ValidatePasswordResult.Error -> {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is ValidatePasswordResult.Success -> {
|
||||
if (result.isValid) {
|
||||
onContent { content ->
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = null,
|
||||
viewState = content.copy(
|
||||
common = content.common.copy(requiresReprompt = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
trySendAction(action.repromptAction.vaultItemAction)
|
||||
}
|
||||
} else {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeleteCipherReceive(action: VaultItemAction.Internal.DeleteCipherReceive) {
|
||||
when (val result = action.result) {
|
||||
is DeleteCipherResult.Error -> {
|
||||
@@ -1367,17 +1151,7 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleRestoreItemClicked() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.RestoreItemClick,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
updateDialogState(VaultItemState.DialogState.RestoreItemDialog)
|
||||
}
|
||||
}
|
||||
updateDialogState(VaultItemState.DialogState.RestoreItemDialog)
|
||||
}
|
||||
|
||||
private fun handleIsIconLoadingDisabledUpdateReceive(
|
||||
@@ -1589,8 +1363,6 @@ data class VaultItemState(
|
||||
* updated.
|
||||
* @property notes Contains general notes taken by the user.
|
||||
* @property customFields A list of custom fields that user has added.
|
||||
* @property requiresReprompt Indicates if a master password prompt is required to view
|
||||
* secure fields.
|
||||
* @property requiresCloneConfirmation Indicates user confirmation is required when
|
||||
* cloning a cipher.
|
||||
* @property currentCipher The cipher that is currently being viewed (nullable).
|
||||
@@ -1608,7 +1380,6 @@ data class VaultItemState(
|
||||
val lastUpdated: String,
|
||||
val notes: String?,
|
||||
val customFields: List<Custom>,
|
||||
val requiresReprompt: Boolean,
|
||||
val requiresCloneConfirmation: Boolean,
|
||||
@IgnoredOnParcel
|
||||
val currentCipher: CipherView? = null,
|
||||
@@ -1915,14 +1686,6 @@ data class VaultItemState(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays the master password dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class MasterPasswordDialog(
|
||||
val action: PasswordRepromptAction,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays the dialog for deleting the item to the user.
|
||||
*/
|
||||
@@ -2062,14 +1825,6 @@ sealed class VaultItemAction {
|
||||
*/
|
||||
data object EditClick : Common()
|
||||
|
||||
/**
|
||||
* The user has submitted their master password.
|
||||
*/
|
||||
data class MasterPasswordSubmit(
|
||||
val masterPassword: String,
|
||||
val action: PasswordRepromptAction,
|
||||
) : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked the refresh button.
|
||||
*/
|
||||
@@ -2337,14 +2092,6 @@ sealed class VaultItemAction {
|
||||
val vaultDataState: DataState<VaultItemStateData>,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the verify password result has been received.
|
||||
*/
|
||||
data class ValidatePasswordReceive(
|
||||
val result: ValidatePasswordResult,
|
||||
val repromptAction: PasswordRepromptAction,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the delete cipher result has been received.
|
||||
*/
|
||||
@@ -2384,175 +2131,3 @@ 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.Common.PasswordHistoryClick]
|
||||
* upon password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data object PasswordHistoryClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.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 an attachment download was clicked.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AttachmentDownloadClick(
|
||||
val attachment: VaultItemState.ViewState.Content.Common.AttachmentItem,
|
||||
) : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.AttachmentDownloadClick(attachment)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.DeleteClick
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.Card.CodeVisibilityClick]
|
||||
* upon password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewCodeClick(
|
||||
val isVisible: Boolean,
|
||||
) : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.ItemType.Card.CodeVisibilityClick(isVisible = isVisible)
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.ItemType.Card.NumberVisibilityClick]
|
||||
* upon password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewNumberClick(
|
||||
val isVisible: Boolean,
|
||||
) : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.ItemType.Card.NumberVisibilityClick(isVisible = isVisible)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should show the confirm restore
|
||||
*/
|
||||
@Parcelize
|
||||
data object RestoreItemClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.RestoreVaultItemClick
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the
|
||||
* [VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked] upon password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewPrivateKeyClicked(
|
||||
val isVisible: Boolean,
|
||||
) : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
|
||||
isVisible = isVisible,
|
||||
)
|
||||
}
|
||||
}
|
||||
-4
@@ -7,7 +7,6 @@ import com.bitwarden.ui.platform.base.util.orZeroWidthSpace
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CardView
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.Fido2Credential
|
||||
@@ -41,7 +40,6 @@ private const val FIDO2_CREDENTIAL_CREATION_TIME_PATTERN: String = "h:mm a"
|
||||
fun CipherView.toViewState(
|
||||
previousState: VaultItemState.ViewState.Content?,
|
||||
isPremiumUser: Boolean,
|
||||
hasMasterPassword: Boolean,
|
||||
totpCodeItemData: TotpCodeItemData?,
|
||||
clock: Clock = Clock.systemDefaultZone(),
|
||||
canDelete: Boolean,
|
||||
@@ -56,8 +54,6 @@ fun CipherView.toViewState(
|
||||
common = VaultItemState.ViewState.Content.Common(
|
||||
currentCipher = this,
|
||||
name = name,
|
||||
requiresReprompt = (reprompt == CipherRepromptType.PASSWORD && hasMasterPassword) &&
|
||||
previousState?.common?.requiresReprompt != false,
|
||||
customFields = fields.orEmpty().map { fieldView ->
|
||||
fieldView.toCustomField(
|
||||
previousState = previousState
|
||||
|
||||
+7
-9
@@ -88,10 +88,7 @@ fun VaultItemListingContent(
|
||||
BitwardenMasterPasswordDialog(
|
||||
onConfirmClick = { password ->
|
||||
masterPasswordRepromptData = null
|
||||
masterPasswordRepromptSubmit(
|
||||
password,
|
||||
data,
|
||||
)
|
||||
masterPasswordRepromptSubmit(password, data)
|
||||
},
|
||||
onDismissRequest = {
|
||||
masterPasswordRepromptData = null
|
||||
@@ -213,14 +210,15 @@ fun VaultItemListingContent(
|
||||
supportingLabelTestTag = it.subtitleTestTag,
|
||||
optionsTestTag = it.optionsTestTag,
|
||||
onClick = {
|
||||
if (it.isTotp && it.shouldShowMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(
|
||||
cipherId = it.id,
|
||||
)
|
||||
} else if (it.isAutofill && it.shouldShowMasterPasswordReprompt) {
|
||||
if (it.isAutofill && it.shouldShowMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.Autofill(
|
||||
cipherId = it.id,
|
||||
)
|
||||
} else if (it.shouldShowMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.ViewItem(
|
||||
id = it.id,
|
||||
itemType = it.itemType,
|
||||
)
|
||||
} else {
|
||||
vaultItemClick(it.id, it.itemType)
|
||||
}
|
||||
|
||||
+11
-19
@@ -1468,13 +1468,8 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
is MasterPasswordRepromptData.Totp -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.NavigateToEditCipher(
|
||||
cipherId = data.cipherId,
|
||||
cipherType = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
)
|
||||
is MasterPasswordRepromptData.ViewItem -> {
|
||||
trySendAction(VaultItemListingsAction.ItemClick(id = data.id, type = data.itemType))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2340,7 +2335,6 @@ data class VaultItemListingState(
|
||||
val optionsTestTag: String,
|
||||
val isAutofill: Boolean,
|
||||
val isCredentialCreation: Boolean,
|
||||
val isTotp: Boolean,
|
||||
val shouldShowMasterPasswordReprompt: Boolean,
|
||||
val itemType: ItemType,
|
||||
) {
|
||||
@@ -2988,28 +2982,26 @@ sealed class VaultItemListingsAction {
|
||||
/**
|
||||
* Data tracking the type of request that triggered a master password reprompt.
|
||||
*/
|
||||
sealed class MasterPasswordRepromptData : Parcelable {
|
||||
sealed class MasterPasswordRepromptData {
|
||||
/**
|
||||
* Autofill was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Autofill(
|
||||
val cipherId: String,
|
||||
) : MasterPasswordRepromptData()
|
||||
|
||||
/**
|
||||
* Totp was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Totp(
|
||||
val cipherId: String,
|
||||
) : MasterPasswordRepromptData()
|
||||
|
||||
/**
|
||||
* A cipher overflow menu item action was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data class OverflowItem(
|
||||
val action: ListingItemOverflowAction.VaultAction,
|
||||
) : MasterPasswordRepromptData()
|
||||
|
||||
/**
|
||||
* An item was selected to be viewed.
|
||||
*/
|
||||
data class ViewItem(
|
||||
val id: String,
|
||||
val itemType: VaultItemListingState.DisplayItem.ItemType,
|
||||
) : MasterPasswordRepromptData()
|
||||
}
|
||||
+1
-1
@@ -94,9 +94,9 @@ sealed class ListingItemOverflowAction : Parcelable {
|
||||
data class ViewClick(
|
||||
val cipherId: String,
|
||||
val cipherType: CipherType,
|
||||
override val requiresPasswordReprompt: Boolean,
|
||||
) : VaultAction() {
|
||||
override val title: Text get() = R.string.view.asText()
|
||||
override val requiresPasswordReprompt: Boolean get() = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-6
@@ -141,7 +141,6 @@ fun VaultData.toViewState(
|
||||
isFido2Creation = createCredentialRequestData != null,
|
||||
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
|
||||
isPremiumUser = isPremiumUser,
|
||||
isTotp = totpData != null,
|
||||
),
|
||||
displayFolderList = folderList.map { folderView ->
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
@@ -341,7 +340,6 @@ private fun List<CipherView>.toDisplayItemList(
|
||||
isFido2Creation: Boolean,
|
||||
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
|
||||
isPremiumUser: Boolean,
|
||||
isTotp: Boolean,
|
||||
): List<VaultItemListingState.DisplayItem> =
|
||||
this.map {
|
||||
it.toDisplayItem(
|
||||
@@ -355,7 +353,6 @@ private fun List<CipherView>.toDisplayItemList(
|
||||
fido2CredentialAutofillView.cipherId == it.id
|
||||
},
|
||||
isPremiumUser = isPremiumUser,
|
||||
isTotp = isTotp,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -379,7 +376,6 @@ private fun CipherView.toDisplayItem(
|
||||
isFido2Creation: Boolean,
|
||||
fido2CredentialAutofillView: Fido2CredentialAutofillView?,
|
||||
isPremiumUser: Boolean,
|
||||
isTotp: Boolean,
|
||||
): VaultItemListingState.DisplayItem =
|
||||
VaultItemListingState.DisplayItem(
|
||||
id = id.orEmpty(),
|
||||
@@ -407,7 +403,6 @@ private fun CipherView.toDisplayItem(
|
||||
optionsTestTag = "CipherOptionsButton",
|
||||
isAutofill = isAutofill,
|
||||
isCredentialCreation = isFido2Creation,
|
||||
isTotp = isTotp,
|
||||
shouldShowMasterPasswordReprompt = (reprompt == CipherRepromptType.PASSWORD) &&
|
||||
hasMasterPassword,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = this.type),
|
||||
@@ -481,7 +476,6 @@ private fun SendView.toDisplayItem(
|
||||
isAutofill = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
isCredentialCreation = false,
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Sends(type = this.type),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ fun CipherView.toOverflowActions(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = this.type,
|
||||
requiresPasswordReprompt = hasMasterPassword,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = cipherId,
|
||||
|
||||
@@ -8,6 +8,10 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -17,6 +21,7 @@ import com.bitwarden.ui.platform.base.util.toListItemCardStyle
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
@@ -34,9 +39,34 @@ private const val TRASH_TYPES_COUNT: Int = 1
|
||||
fun VaultContent(
|
||||
state: VaultState.ViewState.Content,
|
||||
vaultHandlers: VaultHandlers,
|
||||
onOverflowOptionClick: (action: ListingItemOverflowAction.VaultAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// Handles the master password prompt for the row click
|
||||
var masterPasswordRepromptItem by remember {
|
||||
mutableStateOf<VaultState.ViewState.VaultItem?>(value = null)
|
||||
}
|
||||
masterPasswordRepromptItem?.let { action ->
|
||||
BitwardenMasterPasswordDialog(
|
||||
onConfirmClick = { password ->
|
||||
masterPasswordRepromptItem = null
|
||||
vaultHandlers.masterPasswordRepromptSubmit(action, password)
|
||||
},
|
||||
onDismissRequest = { masterPasswordRepromptItem = null },
|
||||
)
|
||||
}
|
||||
// Handles the master password prompt for the overflow clicks
|
||||
var overflowMasterPasswordRepromptAction by remember {
|
||||
mutableStateOf<ListingItemOverflowAction.VaultAction?>(value = null)
|
||||
}
|
||||
overflowMasterPasswordRepromptAction?.let { action ->
|
||||
BitwardenMasterPasswordDialog(
|
||||
onConfirmClick = { password ->
|
||||
overflowMasterPasswordRepromptAction = null
|
||||
vaultHandlers.overflowMasterPasswordRepromptSubmit(action, password)
|
||||
},
|
||||
onDismissRequest = { overflowMasterPasswordRepromptAction = null },
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
@@ -93,13 +123,19 @@ fun VaultContent(
|
||||
trailingLabelIcons = favoriteItem.extraIconList,
|
||||
label = favoriteItem.name(),
|
||||
supportingLabel = favoriteItem.supportingLabel?.invoke(),
|
||||
onClick = { vaultHandlers.vaultItemClick(favoriteItem) },
|
||||
onClick = {
|
||||
if (favoriteItem.shouldShowMasterPasswordReprompt) {
|
||||
masterPasswordRepromptItem = favoriteItem
|
||||
} else {
|
||||
vaultHandlers.vaultItemClick(favoriteItem)
|
||||
}
|
||||
},
|
||||
overflowOptions = favoriteItem.overflowOptions.toImmutableList(),
|
||||
onOverflowOptionClick = { action ->
|
||||
if (favoriteItem.shouldShowMasterPasswordReprompt &&
|
||||
action.requiresPasswordReprompt
|
||||
) {
|
||||
onOverflowOptionClick(action)
|
||||
overflowMasterPasswordRepromptAction = action
|
||||
} else {
|
||||
vaultHandlers.overflowOptionClick(action)
|
||||
}
|
||||
@@ -267,13 +303,19 @@ fun VaultContent(
|
||||
trailingLabelIcons = noFolderItem.extraIconList,
|
||||
label = noFolderItem.name(),
|
||||
supportingLabel = noFolderItem.supportingLabel?.invoke(),
|
||||
onClick = { vaultHandlers.vaultItemClick(noFolderItem) },
|
||||
onClick = {
|
||||
if (noFolderItem.shouldShowMasterPasswordReprompt) {
|
||||
masterPasswordRepromptItem = noFolderItem
|
||||
} else {
|
||||
vaultHandlers.vaultItemClick(noFolderItem)
|
||||
}
|
||||
},
|
||||
overflowOptions = noFolderItem.overflowOptions.toImmutableList(),
|
||||
onOverflowOptionClick = { action ->
|
||||
if (noFolderItem.shouldShowMasterPasswordReprompt &&
|
||||
action.requiresPasswordReprompt
|
||||
) {
|
||||
onOverflowOptionClick(action)
|
||||
overflowMasterPasswordRepromptAction = action
|
||||
} else {
|
||||
vaultHandlers.overflowOptionClick(action)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.BitwardenPullToRefreshState
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.rememberBitwardenPullToRefreshState
|
||||
@@ -69,7 +68,6 @@ import com.x8bit.bitwarden.ui.vault.components.VaultItemSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
@@ -243,21 +241,6 @@ private fun VaultScreenScaffold(
|
||||
)
|
||||
}
|
||||
|
||||
var masterPasswordRepromptAction by remember {
|
||||
mutableStateOf<ListingItemOverflowAction.VaultAction?>(null)
|
||||
}
|
||||
masterPasswordRepromptAction?.let { action ->
|
||||
BitwardenMasterPasswordDialog(
|
||||
onConfirmClick = { password ->
|
||||
masterPasswordRepromptAction = null
|
||||
vaultHandlers.masterPasswordRepromptSubmit(action, password)
|
||||
},
|
||||
onDismissRequest = {
|
||||
masterPasswordRepromptAction = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenScaffold(
|
||||
topBar = {
|
||||
BitwardenMediumTopAppBar(
|
||||
@@ -373,7 +356,6 @@ private fun VaultScreenScaffold(
|
||||
is VaultState.ViewState.Content -> VaultContent(
|
||||
state = viewState,
|
||||
vaultHandlers = vaultHandlers,
|
||||
onOverflowOptionClick = { masterPasswordRepromptAction = it },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
|
||||
@@ -204,6 +204,9 @@ class VaultViewModel @Inject constructor(
|
||||
is VaultAction.DialogDismiss -> handleDialogDismiss()
|
||||
is VaultAction.RefreshPull -> handleRefreshPull()
|
||||
is VaultAction.OverflowOptionClick -> handleOverflowOptionClick(action)
|
||||
is VaultAction.OverflowMasterPasswordRepromptSubmit -> {
|
||||
handleOverflowMasterPasswordRepromptSubmit(action)
|
||||
}
|
||||
|
||||
is VaultAction.MasterPasswordRepromptSubmit -> {
|
||||
handleMasterPasswordRepromptSubmit(action)
|
||||
@@ -506,13 +509,13 @@ class VaultViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordRepromptSubmit(
|
||||
action: VaultAction.MasterPasswordRepromptSubmit,
|
||||
private fun handleOverflowMasterPasswordRepromptSubmit(
|
||||
action: VaultAction.OverflowMasterPasswordRepromptSubmit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.validatePassword(action.password)
|
||||
sendAction(
|
||||
VaultAction.Internal.ValidatePasswordResultReceive(
|
||||
VaultAction.Internal.OverflowValidatePasswordResultReceive(
|
||||
overflowAction = action.overflowAction,
|
||||
result = result,
|
||||
),
|
||||
@@ -520,6 +523,20 @@ class VaultViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordRepromptSubmit(
|
||||
action: VaultAction.MasterPasswordRepromptSubmit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.validatePassword(password = action.password)
|
||||
sendAction(
|
||||
VaultAction.Internal.ValidatePasswordResultReceive(
|
||||
item = action.item,
|
||||
result = result,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyNoteClick(action: ListingItemOverflowAction.VaultAction.CopyNoteClick) {
|
||||
clipboardManager.setText(
|
||||
text = action.notes,
|
||||
@@ -616,6 +633,10 @@ class VaultViewModel @Inject constructor(
|
||||
action,
|
||||
)
|
||||
|
||||
is VaultAction.Internal.OverflowValidatePasswordResultReceive -> {
|
||||
handleOverflowValidatePasswordResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultAction.Internal.ValidatePasswordResultReceive -> {
|
||||
handleValidatePasswordResultReceive(action)
|
||||
}
|
||||
@@ -813,8 +834,8 @@ class VaultViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleValidatePasswordResultReceive(
|
||||
action: VaultAction.Internal.ValidatePasswordResultReceive,
|
||||
private fun handleOverflowValidatePasswordResultReceive(
|
||||
action: VaultAction.Internal.OverflowValidatePasswordResultReceive,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is ValidatePasswordResult.Error -> {
|
||||
@@ -847,6 +868,39 @@ class VaultViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleValidatePasswordResultReceive(
|
||||
action: VaultAction.Internal.ValidatePasswordResultReceive,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is ValidatePasswordResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ValidatePasswordResult.Success -> {
|
||||
if (result.isValid) {
|
||||
trySendAction(VaultAction.VaultItemClick(vaultItem = action.item))
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion VaultAction Handlers
|
||||
}
|
||||
|
||||
@@ -1462,11 +1516,20 @@ sealed class VaultAction {
|
||||
* User submitted their master password to authenticate before continuing with
|
||||
* the selected overflow action.
|
||||
*/
|
||||
data class MasterPasswordRepromptSubmit(
|
||||
data class OverflowMasterPasswordRepromptSubmit(
|
||||
val overflowAction: ListingItemOverflowAction.VaultAction,
|
||||
val password: String,
|
||||
) : VaultAction()
|
||||
|
||||
/**
|
||||
* User submitted their master password to authenticate before continuing with the primary
|
||||
* action.
|
||||
*/
|
||||
data class MasterPasswordRepromptSubmit(
|
||||
val item: VaultState.ViewState.VaultItem,
|
||||
val password: String,
|
||||
) : VaultAction()
|
||||
|
||||
/**
|
||||
* The lifecycle of the VaultScreen has entered a resumed state.
|
||||
*/
|
||||
@@ -1524,11 +1587,19 @@ sealed class VaultAction {
|
||||
/**
|
||||
* Indicates that a result for verifying the user's master password has been received.
|
||||
*/
|
||||
data class ValidatePasswordResultReceive(
|
||||
data class OverflowValidatePasswordResultReceive(
|
||||
val overflowAction: ListingItemOverflowAction.VaultAction,
|
||||
val result: ValidatePasswordResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a result for verifying the user's master password has been received.
|
||||
*/
|
||||
data class ValidatePasswordResultReceive(
|
||||
val item: VaultState.ViewState.VaultItem,
|
||||
val result: ValidatePasswordResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a snackbar data was received.
|
||||
*/
|
||||
|
||||
+15
-3
@@ -36,7 +36,11 @@ data class VaultHandlers(
|
||||
val tryAgainClick: () -> Unit,
|
||||
val dialogDismiss: () -> Unit,
|
||||
val overflowOptionClick: (ListingItemOverflowAction.VaultAction) -> Unit,
|
||||
val masterPasswordRepromptSubmit: (ListingItemOverflowAction.VaultAction, String) -> Unit,
|
||||
val overflowMasterPasswordRepromptSubmit: (
|
||||
ListingItemOverflowAction.VaultAction,
|
||||
String,
|
||||
) -> Unit,
|
||||
val masterPasswordRepromptSubmit: (VaultState.ViewState.VaultItem, String) -> Unit,
|
||||
val dismissImportActionCard: () -> Unit,
|
||||
val importActionCardClick: () -> Unit,
|
||||
val flightRecorderGoToSettingsClick: () -> Unit,
|
||||
@@ -93,10 +97,18 @@ data class VaultHandlers(
|
||||
overflowOptionClick = {
|
||||
viewModel.trySendAction(VaultAction.OverflowOptionClick(it))
|
||||
},
|
||||
masterPasswordRepromptSubmit = { action, password ->
|
||||
overflowMasterPasswordRepromptSubmit = { action, password ->
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowMasterPasswordRepromptSubmit(
|
||||
overflowAction = action,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
},
|
||||
masterPasswordRepromptSubmit = { item, password ->
|
||||
viewModel.trySendAction(
|
||||
VaultAction.MasterPasswordRepromptSubmit(
|
||||
overflowAction = action,
|
||||
item = item,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
|
||||
+11
-6
@@ -197,7 +197,7 @@ fun List<LoginUriView>?.toLoginIconData(
|
||||
/**
|
||||
* Transforms a [CipherView] into a [VaultState.ViewState.VaultItem].
|
||||
*/
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
@Suppress("MagicNumber", "LongMethod", "CyclomaticComplexMethod")
|
||||
private fun CipherView.toVaultItemOrNull(
|
||||
hasMasterPassword: Boolean,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
@@ -220,7 +220,8 @@ private fun CipherView.toVaultItemOrNull(
|
||||
isPremiumUser = isPremiumUser,
|
||||
),
|
||||
extraIconList = toLabelIcons(),
|
||||
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
shouldShowMasterPasswordReprompt = hasMasterPassword &&
|
||||
reprompt == CipherRepromptType.PASSWORD,
|
||||
)
|
||||
|
||||
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
|
||||
@@ -231,7 +232,8 @@ private fun CipherView.toVaultItemOrNull(
|
||||
isPremiumUser = isPremiumUser,
|
||||
),
|
||||
extraIconList = toLabelIcons(),
|
||||
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
shouldShowMasterPasswordReprompt = hasMasterPassword &&
|
||||
reprompt == CipherRepromptType.PASSWORD,
|
||||
)
|
||||
|
||||
CipherType.CARD -> VaultState.ViewState.VaultItem.Card(
|
||||
@@ -246,7 +248,8 @@ private fun CipherView.toVaultItemOrNull(
|
||||
isPremiumUser = isPremiumUser,
|
||||
),
|
||||
extraIconList = toLabelIcons(),
|
||||
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
shouldShowMasterPasswordReprompt = hasMasterPassword &&
|
||||
reprompt == CipherRepromptType.PASSWORD,
|
||||
)
|
||||
|
||||
CipherType.IDENTITY -> VaultState.ViewState.VaultItem.Identity(
|
||||
@@ -263,7 +266,8 @@ private fun CipherView.toVaultItemOrNull(
|
||||
isPremiumUser = isPremiumUser,
|
||||
),
|
||||
extraIconList = toLabelIcons(),
|
||||
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
shouldShowMasterPasswordReprompt = hasMasterPassword &&
|
||||
reprompt == CipherRepromptType.PASSWORD,
|
||||
)
|
||||
|
||||
CipherType.SSH_KEY -> VaultState.ViewState.VaultItem.SshKey(
|
||||
@@ -286,7 +290,8 @@ private fun CipherView.toVaultItemOrNull(
|
||||
isPremiumUser = isPremiumUser,
|
||||
),
|
||||
extraIconList = toLabelIcons(),
|
||||
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
shouldShowMasterPasswordReprompt = hasMasterPassword &&
|
||||
reprompt == CipherRepromptType.PASSWORD,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+43
-7
@@ -13,7 +13,9 @@ 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.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -34,7 +36,9 @@ import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.RegisterScreenDataOnLifecycleEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.rememberBitwardenPullToRefreshState
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
@@ -91,7 +95,10 @@ fun VerificationCodeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
VerificationCodeDialogs(dialogState = state.dialogState)
|
||||
VerificationCodeDialogs(
|
||||
dialogState = state.dialogState,
|
||||
onDismissRequest = verificationCodeHandler.dismissDialog,
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
@@ -133,7 +140,9 @@ fun VerificationCodeScreen(
|
||||
VerificationCodeContent(
|
||||
items = viewState.verificationCodeDisplayItems.toImmutableList(),
|
||||
onCopyClick = verificationCodeHandler.copyClick,
|
||||
itemClick = verificationCodeHandler.itemClick,
|
||||
onItemClick = verificationCodeHandler.itemClick,
|
||||
onMasterPasswordRepromptSubmit = verificationCodeHandler
|
||||
.masterPasswordRepromptSubmit,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -156,11 +165,23 @@ fun VerificationCodeScreen(
|
||||
@Composable
|
||||
private fun VerificationCodeDialogs(
|
||||
dialogState: VerificationCodeState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
is VerificationCodeState.DialogState.Loading -> BitwardenLoadingDialog(
|
||||
text = dialogState.message(),
|
||||
)
|
||||
is VerificationCodeState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
title = dialogState.title?.invoke(),
|
||||
message = dialogState.message(),
|
||||
throwable = dialogState.throwable,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is VerificationCodeState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
text = dialogState.message(),
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
@@ -169,10 +190,21 @@ private fun VerificationCodeDialogs(
|
||||
@Composable
|
||||
private fun VerificationCodeContent(
|
||||
items: ImmutableList<VerificationCodeDisplayItem>,
|
||||
itemClick: (id: String) -> Unit,
|
||||
onItemClick: (id: String) -> Unit,
|
||||
onMasterPasswordRepromptSubmit: (id: String, password: String) -> Unit,
|
||||
onCopyClick: (text: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var masterPasswordRepromptId by remember { mutableStateOf<String?>(value = null) }
|
||||
masterPasswordRepromptId?.let { id ->
|
||||
BitwardenMasterPasswordDialog(
|
||||
onConfirmClick = { password ->
|
||||
onMasterPasswordRepromptSubmit(id, password)
|
||||
masterPasswordRepromptId = null
|
||||
},
|
||||
onDismissRequest = { masterPasswordRepromptId = null },
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
@@ -200,7 +232,11 @@ private fun VerificationCodeContent(
|
||||
hideAuthCode = it.hideAuthCode,
|
||||
onCopyClick = { onCopyClick(it.authCode) },
|
||||
onItemClick = {
|
||||
itemClick(it.id)
|
||||
if (it.hideAuthCode) {
|
||||
masterPasswordRepromptId = it.id
|
||||
} else {
|
||||
onItemClick(it.id)
|
||||
}
|
||||
},
|
||||
cardStyle = items.toListItemCardStyle(index = index, dividerPadding = 56.dp),
|
||||
modifier = Modifier
|
||||
|
||||
+100
-5
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.data.repository.util.baseIconUrl
|
||||
@@ -11,6 +12,7 @@ import com.bitwarden.ui.util.concat
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -25,9 +27,12 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* Handles [VerificationCodeAction],
|
||||
* and launches [VerificationCodeEvent] for the [VerificationCodeScreen].
|
||||
@@ -35,13 +40,15 @@ import javax.inject.Inject
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class VerificationCodeViewModel @Inject constructor(
|
||||
authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val authRepository: AuthRepository,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<VerificationCodeState, VerificationCodeEvent, VerificationCodeAction>(
|
||||
initialState = run {
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
VerificationCodeState(
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
@@ -88,7 +95,9 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
when (action) {
|
||||
is VerificationCodeAction.BackClick -> handleBackClick()
|
||||
is VerificationCodeAction.CopyClick -> handleCopyClick(action)
|
||||
is VerificationCodeAction.DismissDialog -> handleDismissDialog()
|
||||
is VerificationCodeAction.ItemClick -> handleItemClick(action)
|
||||
is VerificationCodeAction.MasterPasswordSubmit -> handleMasterPasswordSubmit(action)
|
||||
is VerificationCodeAction.LockClick -> handleLockClick()
|
||||
is VerificationCodeAction.RefreshClick -> handleRefreshClick()
|
||||
is VerificationCodeAction.RefreshPull -> handleRefreshPull()
|
||||
@@ -112,12 +121,28 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleDismissDialog() {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
|
||||
private fun handleItemClick(action: VerificationCodeAction.ItemClick) {
|
||||
sendEvent(
|
||||
VerificationCodeEvent.NavigateToVaultItem(action.id),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordSubmit(action: VerificationCodeAction.MasterPasswordSubmit) {
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.validatePassword(action.password)
|
||||
sendAction(
|
||||
VerificationCodeAction.Internal.ValidatePasswordResultReceive(
|
||||
result = result,
|
||||
cipherId = action.cipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLockClick() {
|
||||
vaultRepository.lockVaultForCurrentUser(isUserInitiated = true)
|
||||
}
|
||||
@@ -152,14 +177,54 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
|
||||
private fun handleInternalAction(action: VerificationCodeAction.Internal) {
|
||||
when (action) {
|
||||
is VerificationCodeAction.Internal.IconLoadingSettingReceive ->
|
||||
is VerificationCodeAction.Internal.IconLoadingSettingReceive -> {
|
||||
handleIconsSettingReceived(action)
|
||||
}
|
||||
|
||||
is VerificationCodeAction.Internal.PullToRefreshEnableReceive ->
|
||||
is VerificationCodeAction.Internal.PullToRefreshEnableReceive -> {
|
||||
handlePullToRefreshEnableReceive(action)
|
||||
}
|
||||
|
||||
is VerificationCodeAction.Internal.AuthCodesReceive ->
|
||||
is VerificationCodeAction.Internal.AuthCodesReceive -> {
|
||||
handleAuthCodeReceive(action)
|
||||
}
|
||||
|
||||
is VerificationCodeAction.Internal.ValidatePasswordResultReceive -> {
|
||||
handleValidatePasswordResultReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleValidatePasswordResultReceive(
|
||||
action: VerificationCodeAction.Internal.ValidatePasswordResultReceive,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is ValidatePasswordResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VerificationCodeState.DialogState.Error(
|
||||
title = null,
|
||||
message = R.string.generic_error_message.asText(),
|
||||
throwable = result.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ValidatePasswordResult.Success -> {
|
||||
if (result.isValid) {
|
||||
trySendAction(VerificationCodeAction.ItemClick(id = action.cipherId))
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VerificationCodeState.DialogState.Error(
|
||||
title = null,
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +424,15 @@ data class VerificationCodeState(
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
*/
|
||||
sealed class DialogState : Parcelable {
|
||||
/**
|
||||
* Represents an error dialog with the given [title] and [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
val throwable: Throwable? = null,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Represents a loading dialog with the given [message].
|
||||
@@ -457,6 +531,11 @@ sealed class VerificationCodeAction {
|
||||
*/
|
||||
data object BackClick : VerificationCodeAction()
|
||||
|
||||
/**
|
||||
* User has dismissed the dialog.
|
||||
*/
|
||||
data object DismissDialog : VerificationCodeAction()
|
||||
|
||||
/**
|
||||
* User has clicked the copy button.
|
||||
*/
|
||||
@@ -469,6 +548,14 @@ sealed class VerificationCodeAction {
|
||||
*/
|
||||
data class ItemClick(val id: String) : VerificationCodeAction()
|
||||
|
||||
/**
|
||||
* Validates the master password before viewing the cipher.
|
||||
*/
|
||||
data class MasterPasswordSubmit(
|
||||
val cipherId: String,
|
||||
val password: String,
|
||||
) : VerificationCodeAction()
|
||||
|
||||
/**
|
||||
* User has clicked the lock button.
|
||||
*/
|
||||
@@ -519,5 +606,13 @@ sealed class VerificationCodeAction {
|
||||
data class AuthCodesReceive(
|
||||
val verificationCodeData: DataState<List<VerificationCodeItem>>,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* The password validation data was received.
|
||||
*/
|
||||
data class ValidatePasswordResultReceive(
|
||||
val result: ValidatePasswordResult,
|
||||
val cipherId: String,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
+8
@@ -11,10 +11,12 @@ data class VerificationCodeHandlers(
|
||||
val backClick: () -> Unit,
|
||||
val searchIconClick: () -> Unit,
|
||||
val itemClick: (id: String) -> Unit,
|
||||
val masterPasswordRepromptSubmit: (id: String, password: String) -> Unit,
|
||||
val refreshClick: () -> Unit,
|
||||
val syncClick: () -> Unit,
|
||||
val lockClick: () -> Unit,
|
||||
val copyClick: (text: String) -> Unit,
|
||||
val dismissDialog: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
@@ -31,10 +33,16 @@ data class VerificationCodeHandlers(
|
||||
viewModel.trySendAction(VerificationCodeAction.SearchIconClick)
|
||||
},
|
||||
itemClick = { viewModel.trySendAction(VerificationCodeAction.ItemClick(it)) },
|
||||
masterPasswordRepromptSubmit = { cipherId, password ->
|
||||
viewModel.trySendAction(
|
||||
action = VerificationCodeAction.MasterPasswordSubmit(cipherId, password),
|
||||
)
|
||||
},
|
||||
refreshClick = { viewModel.trySendAction(VerificationCodeAction.RefreshClick) },
|
||||
syncClick = { viewModel.trySendAction(VerificationCodeAction.SyncClick) },
|
||||
lockClick = { viewModel.trySendAction(VerificationCodeAction.LockClick) },
|
||||
copyClick = { viewModel.trySendAction(VerificationCodeAction.CopyClick(it)) },
|
||||
dismissDialog = { viewModel.trySendAction(VerificationCodeAction.DismissDialog) },
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -401,11 +401,11 @@ class SearchScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on totp when reprompt is required should show master password dialog`() {
|
||||
fun `clicking on item when reprompt is required should show master password dialog`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||
viewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(
|
||||
createMockDisplayItemForCipher(number = 1, isTotp = true).copy(
|
||||
createMockDisplayItemForCipher(number = 1).copy(
|
||||
shouldDisplayMasterPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
@@ -626,6 +626,7 @@ class SearchScreenTest : BitwardenComposeTest() {
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-1",
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
+5
-6
@@ -650,8 +650,11 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.MasterPasswordRepromptSubmit(
|
||||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.ViewItem(
|
||||
cipherId = cipherId,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(
|
||||
type = CipherType.LOGIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1103,6 +1106,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1140,7 +1144,6 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
isAutofill = false,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
} returns expectedViewState
|
||||
val dataState = DataState.Loaded(
|
||||
@@ -1243,7 +1246,6 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
isAutofill = false,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
} returns expectedViewState
|
||||
mutableVaultDataStateFlow.tryEmit(
|
||||
@@ -1356,7 +1358,6 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
isAutofill = false,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
} returns expectedViewState
|
||||
val dataState = DataState.Error(
|
||||
@@ -1472,7 +1473,6 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
isAutofill = false,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
} returns expectedViewState
|
||||
val dataState = DataState.NoNetwork(
|
||||
@@ -1637,7 +1637,6 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
isAutofill = true,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
} returns expectedViewState
|
||||
val dataState = DataState.Loaded(
|
||||
|
||||
-5
@@ -326,7 +326,6 @@ class SearchTypeDataExtensionsTest {
|
||||
isAutofill = false,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = true,
|
||||
)
|
||||
|
||||
assertEquals(SearchState.ViewState.Empty(message = null), result)
|
||||
@@ -352,7 +351,6 @@ class SearchTypeDataExtensionsTest {
|
||||
isAutofill = false,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -393,7 +391,6 @@ class SearchTypeDataExtensionsTest {
|
||||
isAutofill = true,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -444,7 +441,6 @@ class SearchTypeDataExtensionsTest {
|
||||
isAutofill = false,
|
||||
hasMasterPassword = true,
|
||||
isPremiumUser = true,
|
||||
isTotp = true,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -474,7 +470,6 @@ class SearchTypeDataExtensionsTest {
|
||||
hasMasterPassword = true,
|
||||
isAutofill = false,
|
||||
isPremiumUser = true,
|
||||
isTotp = false,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
||||
+5
-8
@@ -16,7 +16,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
fun createMockDisplayItemForCipher(
|
||||
number: Int,
|
||||
cipherType: CipherType = CipherType.LOGIN,
|
||||
isTotp: Boolean = false,
|
||||
@DrawableRes fallbackIconRes: Int = R.drawable.ic_globe,
|
||||
): SearchState.DisplayItem =
|
||||
when (cipherType) {
|
||||
@@ -47,6 +46,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -72,7 +72,6 @@ fun createMockDisplayItemForCipher(
|
||||
totpCode = "mockTotp-$number",
|
||||
autofillSelectionOptions = emptyList(),
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
isTotp = isTotp,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -101,6 +100,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = CipherType.SECURE_NOTE,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -115,7 +115,6 @@ fun createMockDisplayItemForCipher(
|
||||
totpCode = null,
|
||||
autofillSelectionOptions = emptyList(),
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
isTotp = false,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -144,6 +143,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = CipherType.CARD,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -164,7 +164,6 @@ fun createMockDisplayItemForCipher(
|
||||
totpCode = null,
|
||||
autofillSelectionOptions = emptyList(),
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
isTotp = false,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -193,6 +192,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = CipherType.IDENTITY,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -204,7 +204,6 @@ fun createMockDisplayItemForCipher(
|
||||
totpCode = null,
|
||||
autofillSelectionOptions = emptyList(),
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
isTotp = false,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -228,6 +227,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = CipherType.SSH_KEY,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -239,7 +239,6 @@ fun createMockDisplayItemForCipher(
|
||||
totpCode = null,
|
||||
autofillSelectionOptions = emptyList(),
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
isTotp = false,
|
||||
itemType = SearchState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -296,7 +295,6 @@ fun createMockDisplayItemForSend(
|
||||
totpCode = null,
|
||||
autofillSelectionOptions = emptyList(),
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
isTotp = false,
|
||||
itemType = SearchState.DisplayItem.ItemType.Sends(type = sendType),
|
||||
)
|
||||
}
|
||||
@@ -343,7 +341,6 @@ fun createMockDisplayItemForSend(
|
||||
totpCode = null,
|
||||
autofillSelectionOptions = emptyList(),
|
||||
shouldDisplayMasterPasswordReprompt = false,
|
||||
isTotp = false,
|
||||
itemType = SearchState.DisplayItem.ItemType.Sends(type = sendType),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onSiblings
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.util.asText
|
||||
@@ -222,53 +221,6 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MasterPassword dialog should be displayed according to state`() {
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Master password confirmation").assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Master password confirmation")
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Ok click on master password dialog should emit DismissDialogClick`() {
|
||||
val enteredPassword = "pass1234"
|
||||
val passwordRepromptAction = PasswordRepromptAction.EditClick
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = passwordRepromptAction,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Master password").performTextInput(enteredPassword)
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.MasterPasswordSubmit(
|
||||
masterPassword = enteredPassword,
|
||||
action = passwordRepromptAction,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `name should be displayed according to state`() {
|
||||
EMPTY_VIEW_STATES
|
||||
@@ -3193,7 +3145,6 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
value = true,
|
||||
),
|
||||
),
|
||||
requiresReprompt = true,
|
||||
requiresCloneConfirmation = false,
|
||||
attachments = listOf(
|
||||
VaultItemState.ViewState.Content.Common.AttachmentItem(
|
||||
@@ -3291,7 +3242,6 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
lastUpdated = "12/31/69 06:16 PM",
|
||||
notes = null,
|
||||
customFields = emptyList(),
|
||||
requiresReprompt = true,
|
||||
requiresCloneConfirmation = false,
|
||||
attachments = emptyList(),
|
||||
canDelete = true,
|
||||
|
||||
+463
-1821
File diff suppressed because it is too large.
Load diff
-105
@@ -38,105 +38,12 @@ class CipherViewExtensionsTest {
|
||||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Login Content maintaining re-prompt and visibility state`() {
|
||||
val cipherView = createCipherView(type = CipherType.LOGIN, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true).copy(
|
||||
currentCipher = cipherView,
|
||||
// This re-prompt state should be preserved
|
||||
requiresReprompt = false,
|
||||
),
|
||||
type = createLoginContent(isEmpty = false).copy(
|
||||
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = "password",
|
||||
// This visibility state should be preserved
|
||||
isVisible = true,
|
||||
canViewPassword = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
verificationCode = "123456",
|
||||
totpCode = "testCode",
|
||||
),
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
canRestore = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true).copy(
|
||||
currentCipher = cipherView,
|
||||
requiresReprompt = false,
|
||||
),
|
||||
type = createLoginContent(isEmpty = false).copy(
|
||||
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = "password",
|
||||
isVisible = true,
|
||||
canViewPassword = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
viewState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Login Content without master password reprompt`() {
|
||||
val cipherView = createCipherView(type = CipherType.LOGIN, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = false,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
verificationCode = "123456",
|
||||
totpCode = "testCode",
|
||||
),
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
canRestore = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true).copy(
|
||||
currentCipher = cipherView,
|
||||
requiresReprompt = false,
|
||||
),
|
||||
type = createLoginContent(isEmpty = false),
|
||||
),
|
||||
viewState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Login Content with premium`() {
|
||||
val cipherView = createCipherView(type = CipherType.LOGIN, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
@@ -171,7 +78,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = isPremiumUser,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
@@ -212,7 +118,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = isPremiumUser,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
@@ -248,7 +153,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -277,7 +181,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -309,7 +212,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -351,7 +253,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -398,7 +299,6 @@ class CipherViewExtensionsTest {
|
||||
val result = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -447,7 +347,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -480,7 +379,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -511,7 +409,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -559,7 +456,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
@@ -587,7 +483,6 @@ class CipherViewExtensionsTest {
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
|
||||
-2
@@ -172,7 +172,6 @@ fun createCommonContent(
|
||||
lastUpdated = "1/1/70 12:16 AM",
|
||||
notes = null,
|
||||
customFields = emptyList(),
|
||||
requiresReprompt = true,
|
||||
requiresCloneConfirmation = false,
|
||||
attachments = emptyList(),
|
||||
canDelete = true,
|
||||
@@ -226,7 +225,6 @@ fun createCommonContent(
|
||||
)
|
||||
.toCustomField(null),
|
||||
),
|
||||
requiresReprompt = true,
|
||||
requiresCloneConfirmation = true,
|
||||
attachments = listOf(
|
||||
VaultItemState.ViewState.Content.Common.AttachmentItem(
|
||||
|
||||
-3
@@ -1121,7 +1121,6 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1).copy(
|
||||
isTotp = true,
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
@@ -2373,7 +2372,6 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = null,
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Sends(type = SendType.TEXT),
|
||||
)
|
||||
|
||||
@@ -2400,6 +2398,5 @@ private fun createCipherDisplayItem(number: Int): VaultItemListingState.DisplayI
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = null,
|
||||
isTotp = true,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = CipherType.LOGIN),
|
||||
)
|
||||
+17
-3
@@ -1178,6 +1178,17 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val cipherId = "cipherId-1234"
|
||||
val password = "password"
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.AddTotpLoginItem(
|
||||
data = TotpData(
|
||||
uri = "uri",
|
||||
issuer = "issuer",
|
||||
accountName = "Name-1",
|
||||
secret = "secret",
|
||||
digits = 6,
|
||||
period = 30,
|
||||
algorithm = TotpData.CryptoHashAlgorithm.SHA_1,
|
||||
),
|
||||
)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
@@ -1187,8 +1198,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.MasterPasswordRepromptSubmit(
|
||||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(
|
||||
cipherId = cipherId,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData.ViewItem(
|
||||
id = cipherId,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(
|
||||
type = CipherType.LOGIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1738,6 +1752,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1903,7 +1918,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createMockDisplayItemForCipher(
|
||||
number = 1,
|
||||
secondSubtitleTestTag = "PasskeySite",
|
||||
isTotp = true,
|
||||
),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
|
||||
+5
-9
@@ -12,14 +12,12 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
/**
|
||||
* Create a mock [VaultItemListingState.DisplayItem] with a given [number].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun createMockDisplayItemForCipher(
|
||||
number: Int,
|
||||
cipherType: CipherType = CipherType.LOGIN,
|
||||
subtitle: String? = "mockUsername-$number",
|
||||
secondSubtitleTestTag: String? = null,
|
||||
requiresPasswordReprompt: Boolean = true,
|
||||
isTotp: Boolean = false,
|
||||
): VaultItemListingState.DisplayItem =
|
||||
when (cipherType) {
|
||||
CipherType.LOGIN -> {
|
||||
@@ -51,6 +49,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = cipherType,
|
||||
requiresPasswordReprompt = requiresPasswordReprompt,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -77,7 +76,6 @@ fun createMockDisplayItemForCipher(
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = "LoginCipherIcon",
|
||||
isTotp = isTotp,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -108,6 +106,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = cipherType,
|
||||
requiresPasswordReprompt = requiresPasswordReprompt,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -123,7 +122,6 @@ fun createMockDisplayItemForCipher(
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = "SecureNoteCipherIcon",
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -154,6 +152,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = cipherType,
|
||||
requiresPasswordReprompt = requiresPasswordReprompt,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -175,7 +174,6 @@ fun createMockDisplayItemForCipher(
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = "CardCipherIcon",
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -206,6 +204,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = cipherType,
|
||||
requiresPasswordReprompt = requiresPasswordReprompt,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -218,7 +217,6 @@ fun createMockDisplayItemForCipher(
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = "IdentityCipherIcon",
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -249,6 +247,7 @@ fun createMockDisplayItemForCipher(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = cipherType,
|
||||
requiresPasswordReprompt = requiresPasswordReprompt,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
@@ -261,7 +260,6 @@ fun createMockDisplayItemForCipher(
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = "SshKeyCipherIcon",
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = cipherType),
|
||||
)
|
||||
}
|
||||
@@ -321,7 +319,6 @@ fun createMockDisplayItemForSend(
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = null,
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Sends(type = sendType),
|
||||
)
|
||||
}
|
||||
@@ -371,7 +368,6 @@ fun createMockDisplayItemForSend(
|
||||
isCredentialCreation = false,
|
||||
shouldShowMasterPasswordReprompt = false,
|
||||
iconTestTag = null,
|
||||
isTotp = false,
|
||||
itemType = VaultItemListingState.DisplayItem.ItemType.Sends(type = sendType),
|
||||
)
|
||||
}
|
||||
|
||||
+13
-1
@@ -33,6 +33,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = id,
|
||||
@@ -70,6 +71,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = id,
|
||||
@@ -107,6 +109,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = id,
|
||||
@@ -144,6 +147,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
result,
|
||||
@@ -169,6 +173,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.CARD,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = id,
|
||||
@@ -211,6 +216,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.CARD,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
result,
|
||||
@@ -231,6 +237,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.IDENTITY,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = id,
|
||||
@@ -254,13 +261,14 @@ class CipherViewExtensionsTest {
|
||||
identity = createMockIdentityView(number = 1),
|
||||
)
|
||||
|
||||
val result = cipher.toOverflowActions(hasMasterPassword = true, false)
|
||||
val result = cipher.toOverflowActions(hasMasterPassword = true, isPremiumUser = false)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.IDENTITY,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
result,
|
||||
@@ -283,6 +291,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.SECURE_NOTE,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = id,
|
||||
@@ -315,6 +324,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.SECURE_NOTE,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
result,
|
||||
@@ -346,6 +356,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
result,
|
||||
@@ -377,6 +388,7 @@ class CipherViewExtensionsTest {
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = id,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = id,
|
||||
|
||||
@@ -7,17 +7,20 @@ import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.isPopup
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onChildren
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
@@ -26,6 +29,7 @@ import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.bitwarden.ui.util.assertScrollableNodeDoesNotExist
|
||||
import com.bitwarden.ui.util.onNodeWithTextAfterScroll
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
@@ -51,6 +55,7 @@ import com.x8bit.bitwarden.ui.util.performYesDialogButtonClick
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
@@ -885,6 +890,263 @@ class VaultScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a favorite item with password prompt should prompt for password before dismissing upon Cancel`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = "12345",
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = emptyList(),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
favoriteItems = listOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.assertTextEquals(itemText, userName)
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.MasterPasswordRepromptSubmit(
|
||||
item = vaultItem,
|
||||
password = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a favorite item with password prompt should prompt for password before sending VaultItemClick upon Submit`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val password = "password1234"
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = "12345",
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = emptyList(),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
favoriteItems = listOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.assertTextEquals(itemText, userName)
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performTextInput(text = password)
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.MasterPasswordRepromptSubmit(
|
||||
item = vaultItem,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a favorite item overflow with password prompt should prompt for password before dismissing upon Cancel`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val cipherId = "12345"
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = cipherId,
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = persistentListOf(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
favoriteItems = listOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "View")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.MasterPasswordRepromptSubmit(
|
||||
item = vaultItem,
|
||||
password = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a favorite item overflow with password prompt should prompt for password before sending VaultItemClick upon Submit`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val password = "password1234"
|
||||
val cipherId = "12345"
|
||||
val overflowAction = ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
)
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = cipherId,
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = persistentListOf(overflowAction),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
favoriteItems = persistentListOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "View")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performTextInput(text = password)
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.OverflowMasterPasswordRepromptSubmit(
|
||||
overflowAction = overflowAction,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking a folder item should send FolderClick with the correct item`() {
|
||||
val folderText = "Test Folder"
|
||||
@@ -973,7 +1235,7 @@ class VaultScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking a no folder item should send VaultItemClick with the correct item`() {
|
||||
fun `clicking a no folder item without password prompt should send VaultItemClick`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "BitWarden"
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
@@ -1001,6 +1263,262 @@ class VaultScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a no folder overflow item with password prompt should prompt for password before dismissing upon Cancel`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val cipherId = "12345"
|
||||
val overflowAction = ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
)
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = cipherId,
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = persistentListOf(overflowAction),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
noFolderItems = listOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "View")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.OverflowMasterPasswordRepromptSubmit(
|
||||
overflowAction = overflowAction,
|
||||
password = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a no folder overflow item with password prompt should prompt for password before sending VaultItemClick upon Submit`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val password = "password1234"
|
||||
val cipherId = "12345"
|
||||
val overflowAction = ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = true,
|
||||
)
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = cipherId,
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = persistentListOf(overflowAction),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
noFolderItems = listOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "View")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performTextInput(text = password)
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.OverflowMasterPasswordRepromptSubmit(
|
||||
overflowAction = overflowAction,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a no folder item with password prompt should prompt for password before dismissing upon Cancel`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = "12345",
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = emptyList(),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
noFolderItems = listOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.assertTextEquals(itemText, userName)
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.MasterPasswordRepromptSubmit(
|
||||
item = vaultItem,
|
||||
password = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking a no folder item with password prompt should prompt for password before sending VaultItemClick upon Submit`() {
|
||||
val itemText = "Test Item"
|
||||
val userName = "Bitwarden"
|
||||
val password = "password1234"
|
||||
val vaultItem = VaultState.ViewState.VaultItem.Login(
|
||||
id = "12345",
|
||||
name = itemText.asText(),
|
||||
username = userName.asText(),
|
||||
overflowOptions = emptyList(),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
noFolderItems = listOf(vaultItem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = itemText)
|
||||
.assertTextEquals(itemText, userName)
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performTextInput(text = password)
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.MasterPasswordRepromptSubmit(
|
||||
item = vaultItem,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login item count should update according to state`() {
|
||||
val rowText = "Login"
|
||||
|
||||
+159
-38
@@ -60,6 +60,7 @@ import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
@@ -1683,24 +1684,143 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OverflowOptionClick Vault ViewClick should emit NavigateToUrl`() = runTest {
|
||||
val cipherId = "cipherId-9876"
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
fun `OverflowOptionClick Vault ViewClick without reprompt should emit NavigateToUrl`() =
|
||||
runTest {
|
||||
val cipherId = "cipherId-9876"
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = CipherType.LOGIN,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
VaultEvent.NavigateToVaultItem(
|
||||
itemId = cipherId,
|
||||
type = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OverflowMasterPasswordRepromptSubmit for a request Error should show a generic error dialog`() =
|
||||
runTest {
|
||||
val password = "password"
|
||||
val error = Throwable("Fail!")
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Error(error = error)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowMasterPasswordRepromptSubmit(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
password = password,
|
||||
requiresPasswordReprompt = true,
|
||||
cipherId = "cipherId",
|
||||
),
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
error = error,
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OverflowMasterPasswordRepromptSubmit for a request Success with an invalid password should show an invalid password dialog`() =
|
||||
runTest {
|
||||
val password = "password"
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Success(isValid = false)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowMasterPasswordRepromptSubmit(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
password = password,
|
||||
requiresPasswordReprompt = true,
|
||||
cipherId = "cipherId",
|
||||
),
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OverflowMasterPasswordRepromptSubmit for a request Success with a valid password should continue the action`() =
|
||||
runTest {
|
||||
val password = "password"
|
||||
val cipherId = "cipherId"
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Success(isValid = true)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowMasterPasswordRepromptSubmit(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
password = password,
|
||||
requiresPasswordReprompt = true,
|
||||
cipherId = cipherId,
|
||||
),
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
VaultEvent.NavigateToVaultItem(itemId = cipherId, type = VaultItemCipherType.LOGIN),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(
|
||||
text = password,
|
||||
toastDescriptorOverride = R.string.password.asText(),
|
||||
)
|
||||
organizationEventManager.trackEvent(
|
||||
event = OrganizationEvent.CipherClientCopiedPassword(cipherId = cipherId),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MasterPasswordRepromptSubmit for a request Error should show a generic error dialog`() =
|
||||
@@ -1720,10 +1840,12 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAction.MasterPasswordRepromptSubmit(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
password = password,
|
||||
requiresPasswordReprompt = true,
|
||||
cipherId = "cipherId",
|
||||
item = VaultState.ViewState.VaultItem.Login(
|
||||
id = "cipherId",
|
||||
name = "name".asText(),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
username = null,
|
||||
overflowOptions = persistentListOf(),
|
||||
),
|
||||
password = password,
|
||||
),
|
||||
@@ -1760,10 +1882,11 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAction.MasterPasswordRepromptSubmit(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
password = password,
|
||||
requiresPasswordReprompt = true,
|
||||
cipherId = "cipherId",
|
||||
item = VaultState.ViewState.VaultItem.Card(
|
||||
id = "cipherId",
|
||||
name = "name".asText(),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
overflowOptions = persistentListOf(),
|
||||
),
|
||||
password = password,
|
||||
),
|
||||
@@ -1786,31 +1909,29 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
fun `MasterPasswordRepromptSubmit for a request Success with a valid password should continue the action`() =
|
||||
runTest {
|
||||
val password = "password"
|
||||
val cipherId = "cipherId"
|
||||
val item = VaultState.ViewState.VaultItem.Identity(
|
||||
id = "cipherId",
|
||||
name = "name".asText(),
|
||||
shouldShowMasterPasswordReprompt = true,
|
||||
fullName = null,
|
||||
overflowOptions = persistentListOf(),
|
||||
)
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Success(isValid = true)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAction.MasterPasswordRepromptSubmit(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
VaultAction.MasterPasswordRepromptSubmit(
|
||||
item = item,
|
||||
password = password,
|
||||
requiresPasswordReprompt = true,
|
||||
cipherId = cipherId,
|
||||
),
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(
|
||||
text = password,
|
||||
toastDescriptorOverride = R.string.password.asText(),
|
||||
)
|
||||
organizationEventManager.trackEvent(
|
||||
event = OrganizationEvent.CipherClientCopiedPassword(cipherId = cipherId),
|
||||
assertEquals(
|
||||
VaultEvent.NavigateToVaultItem(itemId = item.id, type = item.type),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -994,6 +994,7 @@ private fun createMockSshKeyVaultItem(number: Int): VaultState.ViewState.VaultIt
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-$number",
|
||||
cipherType = CipherType.SSH_KEY,
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-$number",
|
||||
|
||||
+122
-2
@@ -13,6 +13,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.data.repository.util.baseIconUrl
|
||||
@@ -263,12 +264,12 @@ class VerificationCodeScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a display item should send ItemClick action`() {
|
||||
fun `clicking on a display item without hideAuthCode should send ItemClick action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
createDisplayItem(number = 1, hideAuthCode = false),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -284,6 +285,104 @@ class VerificationCodeScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking on a display item with hideAuthCode should prompt for password before dismissing the dialog upon cancel`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = listOf(
|
||||
createDisplayItem(number = 1, hideAuthCode = true),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Label 1")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(action = any())
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking on a display item with hideAuthCode should prompt for password before sending MasterPasswordSubmit upon submit`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = listOf(
|
||||
createDisplayItem(number = 1, hideAuthCode = true),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Label 1")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performTextInput(text = "password1234")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
action = VerificationCodeAction.MasterPasswordSubmit(
|
||||
cipherId = "1",
|
||||
password = "password1234",
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on copy button should send CopyClick action`() {
|
||||
mutableStateFlow.update {
|
||||
@@ -374,6 +473,27 @@ class VerificationCodeScreenTest : BitwardenComposeTest() {
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error dialog should be displayed according to state`() {
|
||||
val errorMessage = "error"
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule.onNodeWithText(text = errorMessage).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VerificationCodeState.DialogState.Error(
|
||||
title = null,
|
||||
message = errorMessage.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = errorMessage)
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDisplayItem(
|
||||
|
||||
+106
-1
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
@@ -13,6 +14,7 @@ import com.bitwarden.vault.CipherRepromptType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -22,6 +24,8 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
@@ -129,6 +133,86 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MasterPasswordSubmit should validate the password and display an error on failure`() {
|
||||
val viewModel = createViewModel()
|
||||
val password = "password_1234"
|
||||
val throwable = Throwable("Error!")
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Error(error = throwable)
|
||||
viewModel.trySendAction(
|
||||
action = VerificationCodeAction.MasterPasswordSubmit(
|
||||
cipherId = "cipher_id",
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
viewModel.stateFlow.value,
|
||||
initialState.copy(
|
||||
dialogState = VerificationCodeState.DialogState.Error(
|
||||
title = null,
|
||||
message = R.string.generic_error_message.asText(),
|
||||
throwable = throwable,
|
||||
),
|
||||
),
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.validatePassword(password = password)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `MasterPasswordSubmit should validate the password and display an error on invalid password`() {
|
||||
val viewModel = createViewModel()
|
||||
val password = "password_1234"
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Success(isValid = false)
|
||||
viewModel.trySendAction(
|
||||
action = VerificationCodeAction.MasterPasswordSubmit(
|
||||
cipherId = "cipher_id",
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
viewModel.stateFlow.value,
|
||||
initialState.copy(
|
||||
dialogState = VerificationCodeState.DialogState.Error(
|
||||
title = null,
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.validatePassword(password = password)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MasterPasswordSubmit should validate the password and emit NavigateToVaultItem`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val password = "password_1234"
|
||||
val cipherId = "cipher_id"
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Success(isValid = true)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
action = VerificationCodeAction.MasterPasswordSubmit(
|
||||
cipherId = cipherId,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
assertEquals(VerificationCodeEvent.NavigateToVaultItem(id = cipherId), awaitItem())
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.validatePassword(password = password)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LockClick should call lockVaultForCurrentUser`() {
|
||||
every { vaultRepository.lockVaultForCurrentUser(any()) } just runs
|
||||
@@ -481,6 +565,22 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DismissDialog should clear the dialog state`() = runTest {
|
||||
val defaultState = initialState.copy(
|
||||
dialogState = VerificationCodeState.DialogState.Error(
|
||||
title = null,
|
||||
message = "Test".asText(),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(state = defaultState)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(defaultState, awaitItem())
|
||||
viewModel.trySendAction(VerificationCodeAction.DismissDialog)
|
||||
assertEquals(initialState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMockUri() {
|
||||
mockkStatic(Uri::class)
|
||||
val uriMock = mockk<Uri>()
|
||||
@@ -488,8 +588,13 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
every { uriMock.host } returns "www.mockuri.com"
|
||||
}
|
||||
|
||||
private fun createViewModel(): VerificationCodeViewModel =
|
||||
private fun createViewModel(
|
||||
state: VerificationCodeState? = null,
|
||||
): VerificationCodeViewModel =
|
||||
VerificationCodeViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set(key = "state", value = state)
|
||||
},
|
||||
clipboardManager = clipboardManager,
|
||||
vaultRepository = vaultRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
|
||||
Reference in new issue
Block a user