mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-513: View Card Item (#573)
This commit is contained in:
parent
d16e0c6573
commit
7a6088a23d
14 changed files with 819 additions and 24 deletions
|
@ -12,6 +12,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
|||
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType.Companion.fromId
|
||||
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
|
@ -36,6 +37,7 @@ fun CipherView.toViewState(): VaultAddEditState.ViewState =
|
|||
number = card?.number.orEmpty(),
|
||||
brand = card?.brand.toBrandOrDefault(),
|
||||
expirationMonth = card?.expMonth.toExpirationMonthOrDefault(),
|
||||
expirationYear = card?.expYear.orEmpty(),
|
||||
securityCode = card?.code.orEmpty(),
|
||||
)
|
||||
CipherType.IDENTITY -> VaultAddEditState.ViewState.Content.ItemType.Identity(
|
||||
|
@ -109,13 +111,12 @@ private fun String?.toTitleOrDefault(): VaultIdentityTitle =
|
|||
?: VaultIdentityTitle.SELECT
|
||||
|
||||
private fun String?.toBrandOrDefault(): VaultCardBrand =
|
||||
VaultCardBrand
|
||||
.entries
|
||||
.find { it.name == this }
|
||||
this
|
||||
?.findVaultCardBrandWithNameOrNull()
|
||||
?: VaultCardBrand.SELECT
|
||||
|
||||
private fun String?.toExpirationMonthOrDefault(): VaultCardExpirationMonth =
|
||||
VaultCardExpirationMonth
|
||||
.entries
|
||||
.find { it.name == this }
|
||||
.find { it.number == this }
|
||||
?: VaultCardExpirationMonth.SELECT
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordFieldWithActions
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
|
||||
/**
|
||||
* The top level content UI state for the [VaultItemScreen] when viewing a Card cipher.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun VaultItemCardContent(
|
||||
commonState: VaultItemState.ViewState.Content.Common,
|
||||
cardState: VaultItemState.ViewState.Content.ItemType.Card,
|
||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
item {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.item_information),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.name),
|
||||
value = commonState.name,
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
cardState.cardholderName?.let { cardholderName ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.cardholder_name),
|
||||
value = cardholderName,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
cardState.number?.let { number ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordFieldWithActions(
|
||||
label = stringResource(id = R.string.number),
|
||||
value = number,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
actions = {
|
||||
BitwardenIconButtonWithResource(
|
||||
iconRes = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_copy),
|
||||
contentDescription = stringResource(id = R.string.copy_number),
|
||||
),
|
||||
onClick = vaultCardItemTypeHandlers.onCopyNumberClick,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.brand),
|
||||
value = cardState.brand.value(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cardState.expiration?.let { expiration ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.expiration),
|
||||
value = expiration,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cardState.securityCode?.let { securityCode ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordFieldWithActions(
|
||||
label = stringResource(id = R.string.security_code),
|
||||
value = securityCode,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
actions = {
|
||||
BitwardenIconButtonWithResource(
|
||||
iconRes = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_copy),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.copy_security_code,
|
||||
),
|
||||
),
|
||||
onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.notes?.let { notes ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.notes),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.notes),
|
||||
value = notes,
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.custom_fields),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
items(customFields) { customField ->
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
CustomField(
|
||||
customField = customField,
|
||||
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
|
||||
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
|
||||
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
VaultItemUpdateText(
|
||||
header = "${stringResource(id = R.string.date_updated)}: ",
|
||||
text = commonState.lastUpdated,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(88.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||
|
||||
|
@ -137,6 +138,9 @@ fun VaultItemScreen(
|
|||
vaultLoginItemTypeHandlers = remember(viewModel) {
|
||||
VaultLoginItemTypeHandlers.create(viewModel = viewModel)
|
||||
},
|
||||
vaultCardItemTypeHandlers = remember(viewModel) {
|
||||
VaultCardItemTypeHandlers.create(viewModel = viewModel)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -176,6 +180,7 @@ private fun VaultItemContent(
|
|||
viewState: VaultItemState.ViewState,
|
||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
||||
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (viewState) {
|
||||
|
@ -198,7 +203,13 @@ private fun VaultItemContent(
|
|||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.ItemType.Card -> {
|
||||
// TODO UI for viewing Card BIT-513
|
||||
VaultItemCardContent(
|
||||
commonState = viewState.common,
|
||||
cardState = viewState.type,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
vaultCardItemTypeHandlers = vaultCardItemTypeHandlers,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.ItemType.Identity -> {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.CipherView
|
||||
|
@ -17,6 +18,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -65,8 +67,10 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun handleAction(action: VaultItemAction) {
|
||||
Log.d("ramsey", "handleAction: action $action")
|
||||
when (action) {
|
||||
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
|
||||
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
|
||||
is VaultItemAction.Common -> handleCommonActions(action)
|
||||
is VaultItemAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
|
@ -311,6 +315,43 @@ class VaultItemViewModel @Inject constructor(
|
|||
|
||||
//endregion Login Type Handlers
|
||||
|
||||
//region Card Type Handlers
|
||||
|
||||
private fun handleCardTypeActions(action: VaultItemAction.ItemType.Card) {
|
||||
when (action) {
|
||||
VaultItemAction.ItemType.Card.CopyNumberClick -> handleCopyNumberClick()
|
||||
VaultItemAction.ItemType.Card.CopySecurityCodeClick -> handleCopySecurityCodeClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyNumberClick() {
|
||||
onCardContent { content, card ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onCardContent
|
||||
}
|
||||
val number = requireNotNull(card.number)
|
||||
clipboardManager.setText(text = number)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopySecurityCodeClick() {
|
||||
onCardContent { content, card ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onCardContent
|
||||
}
|
||||
val securityCode = requireNotNull(card.securityCode)
|
||||
clipboardManager.setText(text = securityCode)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Card Type Handlers
|
||||
|
||||
//region Internal Type Handlers
|
||||
|
||||
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
||||
|
@ -452,6 +493,21 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun onCardContent(
|
||||
crossinline block: (
|
||||
VaultItemState.ViewState.Content,
|
||||
VaultItemState.ViewState.Content.ItemType.Card,
|
||||
) -> Unit,
|
||||
) {
|
||||
(state.viewState as? VaultItemState.ViewState.Content)
|
||||
?.let { content ->
|
||||
(content.type as? VaultItemState.ViewState.Content.ItemType.Card)
|
||||
?.let { loginContent ->
|
||||
block(content, loginContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -646,8 +702,20 @@ data class VaultItemState(
|
|||
|
||||
/**
|
||||
* Represents the `Card` item type.
|
||||
*
|
||||
* @property cardholderName The cardholder name for the card.
|
||||
* @property number The number for the card.
|
||||
* @property brand The brand for the card.
|
||||
* @property expiration The expiration for the card.
|
||||
* @property securityCode The securityCode for the card.
|
||||
*/
|
||||
data object Card : ItemType()
|
||||
data class Card(
|
||||
val cardholderName: String?,
|
||||
val number: String?,
|
||||
val brand: VaultCardBrand?,
|
||||
val expiration: String?,
|
||||
val securityCode: String?,
|
||||
) : ItemType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -828,6 +896,22 @@ sealed class VaultItemAction {
|
|||
val isVisible: Boolean,
|
||||
) : Login()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions specific to the Card type.
|
||||
*/
|
||||
sealed class Card : ItemType() {
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the number.
|
||||
*/
|
||||
data object CopyNumberClick : Card()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the security code.
|
||||
*/
|
||||
data object CopySecurityCodeClick : Card()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of viewing card
|
||||
* items in a vault.
|
||||
*/
|
||||
data class VaultCardItemTypeHandlers(
|
||||
val onCopyNumberClick: () -> Unit,
|
||||
val onCopySecurityCodeClick: () -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates the [VaultCardItemTypeHandlers] using the [viewModel] to send desired actions.
|
||||
*/
|
||||
fun create(viewModel: VaultItemViewModel): VaultCardItemTypeHandlers =
|
||||
VaultCardItemTypeHandlers(
|
||||
onCopyNumberClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick)
|
||||
},
|
||||
onCopySecurityCodeClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
|
|||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of viewing identity
|
||||
* A collection of handler functions for managing actions within the context of viewing login
|
||||
* items in a vault.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item.util
|
||||
|
||||
import com.bitwarden.core.CardView
|
||||
import com.bitwarden.core.CipherRepromptType
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
|
@ -14,7 +15,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.orZeroWidthSpace
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.TimeZone
|
||||
|
||||
|
@ -64,7 +67,13 @@ fun CipherView.toViewState(
|
|||
}
|
||||
|
||||
CipherType.CARD -> {
|
||||
VaultItemState.ViewState.Content.ItemType.Card
|
||||
VaultItemState.ViewState.Content.ItemType.Card(
|
||||
cardholderName = card?.cardholderName,
|
||||
number = card?.number,
|
||||
brand = card?.cardBrand,
|
||||
expiration = card?.expiration,
|
||||
securityCode = card?.code,
|
||||
)
|
||||
}
|
||||
|
||||
CipherType.IDENTITY -> {
|
||||
|
@ -140,3 +149,16 @@ private val IdentityView.identityName: String?
|
|||
)
|
||||
.joinToString(" ")
|
||||
.orNullIfBlank()
|
||||
|
||||
private val CardView.cardBrand: VaultCardBrand?
|
||||
get() = brand
|
||||
?.findVaultCardBrandWithNameOrNull()
|
||||
.takeUnless { it == VaultCardBrand.SELECT }
|
||||
|
||||
private val CardView.expiration: String?
|
||||
get() = listOfNotNull(
|
||||
expMonth?.padStart(length = 2, padChar = '0'),
|
||||
expYear,
|
||||
)
|
||||
.joinToString("/")
|
||||
.orNullIfBlank()
|
||||
|
|
|
@ -47,7 +47,7 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
|
|||
|
||||
// Fields we always grab from the UI
|
||||
name = common.name,
|
||||
notes = common.notes,
|
||||
notes = common.notes.orNullIfBlank(),
|
||||
favorite = common.favorite,
|
||||
// TODO Use real folder ID (BIT-528)
|
||||
folderId = common.originalCipher?.folderId,
|
||||
|
@ -74,7 +74,7 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView?
|
|||
.takeUnless { month ->
|
||||
month == VaultCardExpirationMonth.SELECT
|
||||
}
|
||||
?.name,
|
||||
?.number,
|
||||
expYear = it.expirationYear.orNullIfBlank(),
|
||||
code = it.securityCode.orNullIfBlank(),
|
||||
brand = it
|
||||
|
|
|
@ -21,3 +21,19 @@ enum class VaultCardBrand(val value: Text) {
|
|||
RUPAY(value = "RuPay".asText()),
|
||||
OTHER(value = R.string.other.asText()),
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [VaultCardBrand] with the provided [String] or null.
|
||||
*/
|
||||
fun String.findVaultCardBrandWithNameOrNull(): VaultCardBrand? =
|
||||
VaultCardBrand
|
||||
.entries
|
||||
.find { vaultCardBrand ->
|
||||
vaultCardBrand.name.lowercaseWithoutSpacesOrUnderScores ==
|
||||
this.lowercaseWithoutSpacesOrUnderScores
|
||||
}
|
||||
|
||||
private val String.lowercaseWithoutSpacesOrUnderScores: String
|
||||
get() = lowercase()
|
||||
.replace(" ", "")
|
||||
.replace("_", "")
|
||||
|
|
|
@ -12,20 +12,60 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.SELECT_TEXT
|
|||
*/
|
||||
enum class VaultCardExpirationMonth(
|
||||
val value: Text,
|
||||
val number: String,
|
||||
) {
|
||||
SELECT(value = SELECT_TEXT),
|
||||
JANUARY(value = R.string.january.dateText("01 - ")),
|
||||
FEBRUARY(value = R.string.february.dateText("02 - ")),
|
||||
MARCH(value = R.string.march.dateText("03 - ")),
|
||||
APRIL(value = R.string.april.dateText("04 - ")),
|
||||
MAY(value = R.string.may.dateText("05 - ")),
|
||||
JUNE(value = R.string.june.dateText("06 - ")),
|
||||
JULY(value = R.string.july.dateText("07 - ")),
|
||||
AUGUST(value = R.string.august.dateText("08 - ")),
|
||||
SEPTEMBER(value = R.string.september.dateText("09 - ")),
|
||||
OCTOBER(value = R.string.october.dateText("10 - ")),
|
||||
NOVEMBER(value = R.string.november.dateText("11 - ")),
|
||||
DECEMBER(value = R.string.december.dateText("12 - ")),
|
||||
SELECT(
|
||||
value = SELECT_TEXT,
|
||||
number = "0",
|
||||
),
|
||||
JANUARY(
|
||||
value = R.string.january.dateText("01 - "),
|
||||
number = "1",
|
||||
),
|
||||
FEBRUARY(
|
||||
value = R.string.february.dateText("02 - "),
|
||||
number = "2",
|
||||
),
|
||||
MARCH(
|
||||
value = R.string.march.dateText("03 - "),
|
||||
number = "3",
|
||||
),
|
||||
APRIL(
|
||||
value = R.string.april.dateText("04 - "),
|
||||
number = "4",
|
||||
),
|
||||
MAY(
|
||||
value = R.string.may.dateText("05 - "),
|
||||
number = "5",
|
||||
),
|
||||
JUNE(
|
||||
value = R.string.june.dateText("06 - "),
|
||||
number = "6",
|
||||
),
|
||||
JULY(
|
||||
value = R.string.july.dateText("07 - "),
|
||||
number = "7",
|
||||
),
|
||||
AUGUST(
|
||||
value = R.string.august.dateText("08 - "),
|
||||
number = "8",
|
||||
),
|
||||
SEPTEMBER(
|
||||
value = R.string.september.dateText("09 - "),
|
||||
number = "9",
|
||||
),
|
||||
OCTOBER(
|
||||
value = R.string.october.dateText("10 - "),
|
||||
number = "10",
|
||||
),
|
||||
NOVEMBER(
|
||||
value = R.string.november.dateText("11 - "),
|
||||
number = "11",
|
||||
),
|
||||
DECEMBER(
|
||||
value = R.string.december.dateText("12 - "),
|
||||
number = "12",
|
||||
),
|
||||
}
|
||||
|
||||
private fun @receiver:StringRes Int.dateText(prefix: String): Text =
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.bitwarden.core.SecureNoteView
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
|
@ -71,6 +72,8 @@ class CipherViewExtensionsTest {
|
|||
type = VaultAddEditState.ViewState.Content.ItemType.Card(
|
||||
cardHolderName = "Bit Warden",
|
||||
number = "4012888888881881",
|
||||
brand = VaultCardBrand.VISA,
|
||||
expirationYear = "2030",
|
||||
securityCode = "123",
|
||||
),
|
||||
),
|
||||
|
|
|
@ -10,8 +10,10 @@ import androidx.compose.ui.test.filterToOne
|
|||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onChildren
|
||||
import androidx.compose.ui.test.onLast
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onSiblings
|
||||
|
@ -25,7 +27,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
|||
import com.x8bit.bitwarden.ui.util.assertScrollableNodeDoesNotExist
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import com.x8bit.bitwarden.ui.util.onFirstNodeWithTextAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -649,7 +653,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on show password click should send CopyPasswordClick`() {
|
||||
fun `in login state, on show password click should send PasswordVisibilityClicked`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) }
|
||||
|
||||
composeTestRule
|
||||
|
@ -1060,6 +1064,170 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in card state, cardholderName should be displayed according to state`() {
|
||||
val cardholderName = "the cardholder name"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_CARD_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(cardholderName).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCardType(currentState) { copy(cardholderName = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(cardholderName)
|
||||
}
|
||||
|
||||
@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 = "number",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Number")
|
||||
.assertTextEquals("Number", "••••••")
|
||||
.assertIsEnabled()
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy number")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Show")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Number")
|
||||
.assertTextEquals("Number", "number")
|
||||
.assertIsEnabled()
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy number")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Hide")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in card state, on copy number click should send CopyNumberClick`() {
|
||||
val number = "123456789"
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||
type = EMPTY_CARD_TYPE.copy(
|
||||
number = number,
|
||||
expiration = "test",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Scroll so we can see the Copy number button but not have it covered by the FAB
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Expiration")
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy number")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in card state, brand should be displayed according to state`() {
|
||||
val visa = "Visa"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_CARD_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(visa).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCardType(currentState) { copy(brand = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(visa)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in card state, expiration should be displayed according to state`() {
|
||||
val expiration = "the expiration"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_CARD_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(expiration).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCardType(currentState) { copy(expiration = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(expiration)
|
||||
}
|
||||
|
||||
@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 = "123",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Security code")
|
||||
.assertTextEquals("Security code", "•••")
|
||||
.assertIsEnabled()
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy security code")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithContentDescription("Show")
|
||||
.onLast()
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Security code")
|
||||
.assertTextEquals("Security code", "123")
|
||||
.assertIsEnabled()
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy security code")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Hide")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in card state, on copy security code click should send CopySecurityCodeClick`() {
|
||||
val number = "1234"
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_CARD_VIEW_STATE.copy(
|
||||
type = EMPTY_CARD_TYPE.copy(
|
||||
securityCode = number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll("Copy security code")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
@ -1112,6 +1280,30 @@ private fun updateIdentityType(
|
|||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateCardType(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.ItemType.Card.() ->
|
||||
VaultItemState.ViewState.Content.ItemType.Card,
|
||||
): VaultItemState {
|
||||
val updatedType = when (val viewState = currentState.viewState) {
|
||||
is VaultItemState.ViewState.Content -> {
|
||||
when (val type = viewState.type) {
|
||||
is VaultItemState.ViewState.Content.ItemType.Card -> {
|
||||
viewState.copy(
|
||||
type = type.transform(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> viewState
|
||||
}
|
||||
}
|
||||
|
||||
else -> viewState
|
||||
}
|
||||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateCommonContent(
|
||||
currentState: VaultItemState,
|
||||
|
@ -1196,6 +1388,15 @@ private val DEFAULT_IDENTITY: VaultItemState.ViewState.Content.ItemType.Identity
|
|||
address = "the address",
|
||||
)
|
||||
|
||||
private val DEFAULT_CARD: VaultItemState.ViewState.Content.ItemType.Card =
|
||||
VaultItemState.ViewState.Content.ItemType.Card(
|
||||
cardholderName = "the cardholder name",
|
||||
number = "the number",
|
||||
brand = VaultCardBrand.VISA,
|
||||
expiration = "the expiration",
|
||||
securityCode = "the security code",
|
||||
)
|
||||
|
||||
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
name = "cipher",
|
||||
|
@ -1229,6 +1430,15 @@ private val EMPTY_IDENTITY_TYPE: VaultItemState.ViewState.Content.ItemType.Ident
|
|||
address = "",
|
||||
)
|
||||
|
||||
private val EMPTY_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
|
||||
VaultItemState.ViewState.Content.ItemType.Card(
|
||||
cardholderName = "",
|
||||
number = "",
|
||||
brand = VaultCardBrand.SELECT,
|
||||
expiration = "",
|
||||
securityCode = "",
|
||||
)
|
||||
|
||||
private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
|
@ -1241,6 +1451,12 @@ private val EMPTY_IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content =
|
|||
type = EMPTY_IDENTITY_TYPE,
|
||||
)
|
||||
|
||||
private val EMPTY_CARD_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
type = EMPTY_CARD_TYPE,
|
||||
)
|
||||
|
||||
private val EMPTY_SECURE_NOTE_VIEW_STATE =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
|
@ -1259,6 +1475,12 @@ private val DEFAULT_IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content =
|
|||
common = DEFAULT_COMMON,
|
||||
)
|
||||
|
||||
private val DEFAULT_CARD_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
type = DEFAULT_CARD,
|
||||
common = DEFAULT_COMMON,
|
||||
)
|
||||
|
||||
private val DEFAULT_SECURE_NOTE_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON,
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
|||
import com.x8bit.bitwarden.ui.vault.feature.item.util.createCommonContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.createLoginContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
@ -588,6 +589,106 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class CardActions {
|
||||
private lateinit var viewModel: VaultItemViewModel
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
viewState = CARD_VIEW_STATE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyNumberClick 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) } returns CARD_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(cardState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick)
|
||||
assertEquals(
|
||||
cardState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopyNumberClick should call setText on the ClipboardManager when re-prompt is not required`() {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
} 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)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = "12345436")
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopySecurityCodeClick 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) } returns CARD_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(cardState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||
assertEquals(
|
||||
cardState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopySecurityCodeClick should call setText on the ClipboardManager when re-prompt is not required`() {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
} 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)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = "987")
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: VaultItemState?,
|
||||
vaultItemId: String = VAULT_ITEM_ID,
|
||||
|
@ -665,6 +766,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
isPremiumUser = true,
|
||||
)
|
||||
|
||||
private val DEFAULT_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
|
||||
VaultItemState.ViewState.Content.ItemType.Card(
|
||||
cardholderName = "mockName",
|
||||
number = "12345436",
|
||||
brand = VaultCardBrand.VISA,
|
||||
expiration = "03/2027",
|
||||
securityCode = "987",
|
||||
)
|
||||
|
||||
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
name = "login cipher",
|
||||
|
@ -703,5 +813,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
common = DEFAULT_COMMON,
|
||||
type = DEFAULT_LOGIN_TYPE,
|
||||
)
|
||||
|
||||
private val CARD_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON,
|
||||
type = DEFAULT_CARD_TYPE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package com.x8bit.bitwarden.ui.vault.model
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultCardBrandTest {
|
||||
|
||||
@Test
|
||||
fun `findVaultCardBrandWithNameOrNull should return matching brand, regardless of format`() {
|
||||
val names = listOf(
|
||||
"UNIONpay",
|
||||
"AMERICAN_EXPRESS",
|
||||
"diNERs cLub",
|
||||
"rupay",
|
||||
"nothing card",
|
||||
)
|
||||
|
||||
val result = names.map { it.findVaultCardBrandWithNameOrNull() }
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
VaultCardBrand.UNIONPAY,
|
||||
VaultCardBrand.AMERICAN_EXPRESS,
|
||||
VaultCardBrand.DINERS_CLUB,
|
||||
VaultCardBrand.RUPAY,
|
||||
null,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue