mirror of
https://github.com/bitwarden/android.git
synced 2024-11-28 22:08:49 +03:00
Add password reprompt for card number and code (#3350)
This commit is contained in:
parent
94b56f624f
commit
949768ac95
6 changed files with 467 additions and 26 deletions
|
@ -75,13 +75,15 @@ fun VaultItemCardContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cardState.number?.let { number ->
|
cardState.number?.let { numberData ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenPasswordFieldWithActions(
|
BitwardenPasswordFieldWithActions(
|
||||||
label = stringResource(id = R.string.number),
|
label = stringResource(id = R.string.number),
|
||||||
value = number,
|
value = numberData.number,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
|
showPassword = numberData.isVisible,
|
||||||
|
showPasswordChange = vaultCardItemTypeHandlers.onShowNumberClick,
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
actions = {
|
actions = {
|
||||||
|
@ -137,13 +139,15 @@ fun VaultItemCardContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cardState.securityCode?.let { securityCode ->
|
cardState.securityCode?.let { securityCodeData ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenPasswordFieldWithActions(
|
BitwardenPasswordFieldWithActions(
|
||||||
label = stringResource(id = R.string.security_code),
|
label = stringResource(id = R.string.security_code),
|
||||||
value = securityCode,
|
value = securityCodeData.code,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
|
showPassword = securityCodeData.isVisible,
|
||||||
|
showPasswordChange = vaultCardItemTypeHandlers.onShowSecurityCodeClick,
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
actions = {
|
actions = {
|
||||||
|
|
|
@ -679,14 +679,58 @@ class VaultItemViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun handleCardTypeActions(action: VaultItemAction.ItemType.Card) {
|
private fun handleCardTypeActions(action: VaultItemAction.ItemType.Card) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
is VaultItemAction.ItemType.Card.CodeVisibilityClick -> {
|
||||||
|
handleCodeVisibilityClick(action)
|
||||||
|
}
|
||||||
|
|
||||||
VaultItemAction.ItemType.Card.CopyNumberClick -> handleCopyNumberClick()
|
VaultItemAction.ItemType.Card.CopyNumberClick -> handleCopyNumberClick()
|
||||||
VaultItemAction.ItemType.Card.CopySecurityCodeClick -> handleCopySecurityCodeClick()
|
VaultItemAction.ItemType.Card.CopySecurityCodeClick -> handleCopySecurityCodeClick()
|
||||||
|
is VaultItemAction.ItemType.Card.NumberVisibilityClick -> {
|
||||||
|
handleNumberVisibilityClick(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCodeVisibilityClick(
|
||||||
|
action: VaultItemAction.ItemType.Card.CodeVisibilityClick,
|
||||||
|
) {
|
||||||
|
onCardContent { content, card ->
|
||||||
|
if (content.common.requiresReprompt) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
|
action = PasswordRepromptAction.ViewCodeClick(
|
||||||
|
isVisible = action.isVisible,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@onCardContent
|
||||||
|
}
|
||||||
|
mutableStateFlow.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
viewState = content.copy(
|
||||||
|
type = card.copy(
|
||||||
|
securityCode = card.securityCode?.copy(
|
||||||
|
isVisible = action.isVisible,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (action.isVisible) {
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
event = OrganizationEvent.CipherClientToggledCardCodeVisible(
|
||||||
|
cipherId = state.vaultItemId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCopyNumberClick() {
|
private fun handleCopyNumberClick() {
|
||||||
onCardContent { content, card ->
|
onCardContent { content, card ->
|
||||||
val number = requireNotNull(card.number)
|
val number = requireNotNull(card.number).number
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -703,7 +747,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun handleCopySecurityCodeClick() {
|
private fun handleCopySecurityCodeClick() {
|
||||||
onCardContent { content, card ->
|
onCardContent { content, card ->
|
||||||
val securityCode = requireNotNull(card.securityCode)
|
val securityCode = requireNotNull(card.securityCode).code
|
||||||
if (content.common.requiresReprompt) {
|
if (content.common.requiresReprompt) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -718,6 +762,43 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleNumberVisibilityClick(
|
||||||
|
action: VaultItemAction.ItemType.Card.NumberVisibilityClick,
|
||||||
|
) {
|
||||||
|
onCardContent { content, card ->
|
||||||
|
if (content.common.requiresReprompt) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
|
action = PasswordRepromptAction.ViewNumberClick(
|
||||||
|
isVisible = action.isVisible,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@onCardContent
|
||||||
|
}
|
||||||
|
mutableStateFlow.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
viewState = content.copy(
|
||||||
|
type = card.copy(
|
||||||
|
number = card.number?.copy(
|
||||||
|
isVisible = action.isVisible,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (action.isVisible) {
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
event = OrganizationEvent.CipherClientToggledCardNumberVisible(
|
||||||
|
cipherId = state.vaultItemId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//endregion Card Type Handlers
|
//endregion Card Type Handlers
|
||||||
|
|
||||||
//region Internal Type Handlers
|
//region Internal Type Handlers
|
||||||
|
@ -1276,11 +1357,36 @@ data class VaultItemState(
|
||||||
*/
|
*/
|
||||||
data class Card(
|
data class Card(
|
||||||
val cardholderName: String?,
|
val cardholderName: String?,
|
||||||
val number: String?,
|
val number: NumberData?,
|
||||||
val brand: VaultCardBrand?,
|
val brand: VaultCardBrand?,
|
||||||
val expiration: String?,
|
val expiration: String?,
|
||||||
val securityCode: String?,
|
val securityCode: CodeData?,
|
||||||
) : ItemType()
|
) : ItemType() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the number data.
|
||||||
|
*
|
||||||
|
* @property number The card number itself.
|
||||||
|
* @property isVisible Whether or not it is currently visible.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class NumberData(
|
||||||
|
val number: String,
|
||||||
|
val isVisible: Boolean,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the code data.
|
||||||
|
*
|
||||||
|
* @property code The security code itself.
|
||||||
|
* @property isVisible Whether or not it is currently visible.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class CodeData(
|
||||||
|
val code: String,
|
||||||
|
val isVisible: Boolean,
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1581,6 +1687,11 @@ sealed class VaultItemAction {
|
||||||
*/
|
*/
|
||||||
sealed class Card : ItemType() {
|
sealed class Card : ItemType() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked to display the code.
|
||||||
|
*/
|
||||||
|
data class CodeVisibilityClick(val isVisible: Boolean) : Card()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user has clicked the copy button for the number.
|
* The user has clicked the copy button for the number.
|
||||||
*/
|
*/
|
||||||
|
@ -1590,6 +1701,11 @@ sealed class VaultItemAction {
|
||||||
* The user has clicked the copy button for the security code.
|
* The user has clicked the copy button for the security code.
|
||||||
*/
|
*/
|
||||||
data object CopySecurityCodeClick : Card()
|
data object CopySecurityCodeClick : Card()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked to display the Number.
|
||||||
|
*/
|
||||||
|
data class NumberVisibilityClick(val isVisible: Boolean) : Card()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1755,6 +1871,30 @@ sealed class PasswordRepromptAction : Parcelable {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Indicates that we should launch the
|
||||||
* [VaultItemAction.ItemType.Login.PasswordVisibilityClicked] upon password validation.
|
* [VaultItemAction.ItemType.Login.PasswordVisibilityClicked] upon password validation.
|
||||||
|
|
|
@ -10,6 +10,8 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
||||||
data class VaultCardItemTypeHandlers(
|
data class VaultCardItemTypeHandlers(
|
||||||
val onCopyNumberClick: () -> Unit,
|
val onCopyNumberClick: () -> Unit,
|
||||||
val onCopySecurityCodeClick: () -> Unit,
|
val onCopySecurityCodeClick: () -> Unit,
|
||||||
|
val onShowNumberClick: (Boolean) -> Unit,
|
||||||
|
val onShowSecurityCodeClick: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@ -24,6 +26,14 @@ data class VaultCardItemTypeHandlers(
|
||||||
onCopySecurityCodeClick = {
|
onCopySecurityCodeClick = {
|
||||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||||
},
|
},
|
||||||
|
onShowNumberClick = {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.Card.NumberVisibilityClick(it),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onShowSecurityCodeClick = {
|
||||||
|
viewModel.trySendAction(VaultItemAction.ItemType.Card.CodeVisibilityClick(it))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,10 +115,20 @@ fun CipherView.toViewState(
|
||||||
CipherType.CARD -> {
|
CipherType.CARD -> {
|
||||||
VaultItemState.ViewState.Content.ItemType.Card(
|
VaultItemState.ViewState.Content.ItemType.Card(
|
||||||
cardholderName = card?.cardholderName,
|
cardholderName = card?.cardholderName,
|
||||||
number = card?.number,
|
number = card?.number?.let {
|
||||||
|
VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
|
number = it,
|
||||||
|
isVisible = false,
|
||||||
|
)
|
||||||
|
},
|
||||||
brand = card?.cardBrand,
|
brand = card?.cardBrand,
|
||||||
expiration = card?.expiration,
|
expiration = card?.expiration,
|
||||||
securityCode = card?.code,
|
securityCode = card?.code?.let {
|
||||||
|
VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = it,
|
||||||
|
isVisible = false,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1837,14 +1837,17 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `in card state the number should be displayed according to state`() {
|
fun `in card state, on show number click should send NumberVisibilityClick`() {
|
||||||
composeTestRule.assertScrollableNodeDoesNotExist("Number")
|
composeTestRule.assertScrollableNodeDoesNotExist("Number")
|
||||||
|
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
type = EMPTY_CARD_TYPE.copy(
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
|
number = VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
number = "number",
|
number = "number",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -1862,6 +1865,54 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.Card.NumberVisibilityClick(isVisible = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in card state the number should be displayed according to state`() {
|
||||||
|
composeTestRule.assertScrollableNodeDoesNotExist("Number")
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
|
number = VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
|
number = "number",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll("Number")
|
||||||
|
.assertTextEquals("Number", "••••••")
|
||||||
|
.assertIsEnabled()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("Copy number")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("Show")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
|
number = VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
|
number = "number",
|
||||||
|
isVisible = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Number")
|
.onNodeWithText("Number")
|
||||||
.assertTextEquals("Number", "number")
|
.assertTextEquals("Number", "number")
|
||||||
|
@ -1881,7 +1932,10 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
type = EMPTY_CARD_TYPE.copy(
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
|
number = VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
number = number,
|
number = number,
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
expiration = "test",
|
expiration = "test",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1927,14 +1981,17 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `in card state the security code should be displayed according to state`() {
|
fun `in card state, on show code click should send CodeVisibilityClick`() {
|
||||||
composeTestRule.assertScrollableNodeDoesNotExist("Security code")
|
composeTestRule.assertScrollableNodeDoesNotExist("Security code")
|
||||||
|
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
type = EMPTY_CARD_TYPE.copy(
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
securityCode = "123",
|
securityCode = VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = "123",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -1953,6 +2010,55 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.Card.CodeVisibilityClick(isVisible = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in card state the security code should be displayed according to state`() {
|
||||||
|
composeTestRule.assertScrollableNodeDoesNotExist("Security code")
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
|
securityCode = VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = "123",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll("Security code")
|
||||||
|
.assertTextEquals("Security code", "•••")
|
||||||
|
.assertIsEnabled()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("Copy security code")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithContentDescription("Show")
|
||||||
|
.onLast()
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
|
securityCode = VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = "123",
|
||||||
|
isVisible = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Security code")
|
.onNodeWithText("Security code")
|
||||||
.assertTextEquals("Security code", "123")
|
.assertTextEquals("Security code", "123")
|
||||||
|
@ -1967,12 +2073,15 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `in card state, on copy security code click should send CopySecurityCodeClick`() {
|
fun `in card state, on copy security code click should send CopySecurityCodeClick`() {
|
||||||
val number = "1234"
|
val code = "1234"
|
||||||
mutableStateFlow.update { currentState ->
|
mutableStateFlow.update { currentState ->
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||||
type = EMPTY_CARD_TYPE.copy(
|
type = EMPTY_CARD_TYPE.copy(
|
||||||
securityCode = number,
|
securityCode = VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = code,
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -2167,10 +2276,16 @@ private val DEFAULT_IDENTITY: VaultItemState.ViewState.Content.ItemType.Identity
|
||||||
private val DEFAULT_CARD: VaultItemState.ViewState.Content.ItemType.Card =
|
private val DEFAULT_CARD: VaultItemState.ViewState.Content.ItemType.Card =
|
||||||
VaultItemState.ViewState.Content.ItemType.Card(
|
VaultItemState.ViewState.Content.ItemType.Card(
|
||||||
cardholderName = "the cardholder name",
|
cardholderName = "the cardholder name",
|
||||||
|
number = VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
number = "the number",
|
number = "the number",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
brand = VaultCardBrand.VISA,
|
brand = VaultCardBrand.VISA,
|
||||||
expiration = "the expiration",
|
expiration = "the expiration",
|
||||||
securityCode = "the security code",
|
securityCode = VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = "the security code",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
||||||
|
@ -2212,10 +2327,16 @@ private val EMPTY_IDENTITY_TYPE: VaultItemState.ViewState.Content.ItemType.Ident
|
||||||
private val EMPTY_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
|
private val EMPTY_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
|
||||||
VaultItemState.ViewState.Content.ItemType.Card(
|
VaultItemState.ViewState.Content.ItemType.Card(
|
||||||
cardholderName = "",
|
cardholderName = "",
|
||||||
|
number = VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
number = "",
|
number = "",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
brand = VaultCardBrand.SELECT,
|
brand = VaultCardBrand.SELECT,
|
||||||
expiration = "",
|
expiration = "",
|
||||||
securityCode = "",
|
securityCode = VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = "",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||||
|
|
|
@ -1925,7 +1925,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
cardState.copy(
|
cardState.copy(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
action = PasswordRepromptAction.CopyClick(
|
action = PasswordRepromptAction.CopyClick(
|
||||||
value = requireNotNull(DEFAULT_CARD_TYPE.number),
|
value = requireNotNull(DEFAULT_CARD_TYPE.number).number,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1972,6 +1972,81 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NumberVisibilityClick should show password dialog when re-prompt is required`() =
|
||||||
|
runTest {
|
||||||
|
val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE)
|
||||||
|
val mockCipherView = mockk<CipherView> {
|
||||||
|
every {
|
||||||
|
toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
} returns CARD_VIEW_STATE
|
||||||
|
}
|
||||||
|
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||||
|
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||||
|
|
||||||
|
assertEquals(cardState, viewModel.stateFlow.value)
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.Card.NumberVisibilityClick(isVisible = true),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
cardState.copy(
|
||||||
|
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
|
action = PasswordRepromptAction.ViewNumberClick(isVisible = true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
mockCipherView.toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `on NumberVisibilityClick should call trackEvent on the OrganizationEventManager and update the ViewState when re-prompt is not required`() {
|
||||||
|
val mockCipherView = mockk<CipherView> {
|
||||||
|
every {
|
||||||
|
toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
} returns createViewState(
|
||||||
|
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||||
|
type = DEFAULT_CARD_TYPE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
every { clipboardManager.setText(text = "12345436") } just runs
|
||||||
|
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||||
|
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.Card.NumberVisibilityClick(isVisible = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
event = OrganizationEvent.CipherClientToggledCardNumberVisible(
|
||||||
|
cipherId = VAULT_ITEM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mockCipherView.toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on CopySecurityCodeClick should show password dialog when re-prompt is required`() =
|
fun `on CopySecurityCodeClick should show password dialog when re-prompt is required`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
@ -1994,7 +2069,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
cardState.copy(
|
cardState.copy(
|
||||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
action = PasswordRepromptAction.CopyClick(
|
action = PasswordRepromptAction.CopyClick(
|
||||||
value = requireNotNull(DEFAULT_CARD_TYPE.securityCode),
|
value = requireNotNull(DEFAULT_CARD_TYPE.securityCode).code,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -2040,6 +2115,81 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on CodeVisibilityClick should show password dialog when re-prompt is required`() =
|
||||||
|
runTest {
|
||||||
|
val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE)
|
||||||
|
val mockCipherView = mockk<CipherView> {
|
||||||
|
every {
|
||||||
|
toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
} returns CARD_VIEW_STATE
|
||||||
|
}
|
||||||
|
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||||
|
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||||
|
|
||||||
|
assertEquals(cardState, viewModel.stateFlow.value)
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.Card.CodeVisibilityClick(isVisible = true),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
cardState.copy(
|
||||||
|
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||||
|
action = PasswordRepromptAction.ViewCodeClick(isVisible = true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
mockCipherView.toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `on CodeVisibilityClick should call trackEvent on the OrganizationEventManager and update the ViewState when re-prompt is not required`() {
|
||||||
|
val mockCipherView = mockk<CipherView> {
|
||||||
|
every {
|
||||||
|
toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
} returns createViewState(
|
||||||
|
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||||
|
type = DEFAULT_CARD_TYPE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
every { clipboardManager.setText(text = "987") } just runs
|
||||||
|
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||||
|
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.Card.CodeVisibilityClick(isVisible = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
event = OrganizationEvent.CipherClientToggledCardCodeVisible(
|
||||||
|
cipherId = VAULT_ITEM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mockCipherView.toViewState(
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@ -2313,10 +2463,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
private val DEFAULT_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
|
private val DEFAULT_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
|
||||||
VaultItemState.ViewState.Content.ItemType.Card(
|
VaultItemState.ViewState.Content.ItemType.Card(
|
||||||
cardholderName = "mockName",
|
cardholderName = "mockName",
|
||||||
|
number = VaultItemState.ViewState.Content.ItemType.Card.NumberData(
|
||||||
number = "12345436",
|
number = "12345436",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
brand = VaultCardBrand.VISA,
|
brand = VaultCardBrand.VISA,
|
||||||
expiration = "03/2027",
|
expiration = "03/2027",
|
||||||
securityCode = "987",
|
securityCode = VaultItemState.ViewState.Content.ItemType.Card.CodeData(
|
||||||
|
code = "987",
|
||||||
|
isVisible = false,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||||
|
|
Loading…
Reference in a new issue