BIT-507: Create Card UI (#497)

This commit is contained in:
Ramsey Smith 2024-01-05 14:02:46 -07:00 committed by Álison Fernandes
parent c964d8c830
commit 8d5de22c72
22 changed files with 1135 additions and 62 deletions

View file

@ -47,6 +47,8 @@ import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTrans
* @param showPasswordTestTag The test tag to be used on the show password button (testing tool).
* @param autoFocus When set to true, the view will request focus after the first recomposition.
* Setting this to true on multiple fields at once may have unexpected consequences.
* @param keyboardType The type of keyboard the user has access to when inputting values into
* the password field.
*/
@Composable
fun BitwardenPasswordField(
@ -61,6 +63,7 @@ fun BitwardenPasswordField(
hint: String? = null,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
) {
val focusRequester = remember { FocusRequester() }
OutlinedTextField(
@ -76,7 +79,7 @@ fun BitwardenPasswordField(
},
singleLine = singleLine,
readOnly = readOnly,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
supportingText = hint?.let {
{
Text(
@ -129,6 +132,8 @@ fun BitwardenPasswordField(
* @param showPasswordTestTag The test tag to be used on the show password button (testing tool).
* @param autoFocus When set to true, the view will request focus after the first recomposition.
* Setting this to true on multiple fields at once may have unexpected consequences.
* @param keyboardType The type of keyboard the user has access to when inputting values into
* the password field.
*/
@Composable
fun BitwardenPasswordField(
@ -142,6 +147,7 @@ fun BitwardenPasswordField(
initialShowPassword: Boolean = false,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
) {
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
BitwardenPasswordField(
@ -156,6 +162,7 @@ fun BitwardenPasswordField(
hint = hint,
showPasswordTestTag = showPasswordTestTag,
autoFocus = autoFocus,
keyboardType = keyboardType,
)
}

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@ -15,10 +16,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.util.maxDialogHeight
/**
* Displays a dialog with a title and "Cancel" button.
@ -28,6 +31,7 @@ import com.x8bit.bitwarden.R
* @param selectionItems Lambda containing selection items to show to the user. See
* [BitwardenSelectionRow].
*/
@Suppress("LongMethod")
@Composable
fun BitwardenSelectionDialog(
title: String,
@ -37,12 +41,18 @@ fun BitwardenSelectionDialog(
Dialog(
onDismissRequest = onDismissRequest,
) {
val configuration = LocalConfiguration.current
val scrollState = rememberScrollState()
Column(
modifier = Modifier.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(28.dp),
),
modifier = Modifier
.requiredHeightIn(
max = configuration.maxDialogHeight,
)
// This background is necessary for the dialog to not be transparent.
.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(28.dp),
),
horizontalAlignment = Alignment.End,
) {
Text(

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.ui.platform.components.util
import android.content.res.Configuration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Provides the maximum height [Dp] common for all dialogs with a given [Configuration].
*/
val Configuration.maxDialogHeight: Dp
get() = when (orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 312.dp
Configuration.ORIENTATION_PORTRAIT -> 542.dp
Configuration.ORIENTATION_UNDEFINED -> Dp.Unspecified
Configuration.ORIENTATION_SQUARE -> Dp.Unspecified
else -> Dp.Unspecified
}

View file

@ -0,0 +1,286 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitchWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* The UI for adding and editing a card cipher.
*/
@Suppress("LongMethod")
fun LazyListScope.vaultAddEditCardItems(
commonState: VaultAddEditState.ViewState.Content.Common,
cardState: VaultAddEditState.ViewState.Content.ItemType.Card,
commonHandlers: VaultAddEditCommonHandlers,
cardHandlers: VaultAddEditCardTypeHandlers,
isAddItemMode: Boolean,
) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = commonHandlers.onNameTextChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.cardholder_name),
value = cardState.cardHolderName,
onValueChange = cardHandlers.onCardHolderNameTextChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.number),
value = cardState.number,
onValueChange = cardHandlers.onNumberTextChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
val resources = LocalContext.current.resources
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.brand),
options = VaultCardBrand
.entries
.map { it.value() }
.toImmutableList(),
selectedOption = cardState.brand.value(),
onOptionSelected = { selectedString ->
cardHandlers.onBrandSelected(
VaultCardBrand
.entries
.first { it.value.toString(resources) == selectedString },
)
},
modifier = Modifier.padding(horizontal = 16.dp),
)
}
item {
val resources = LocalContext.current.resources
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.expiration_month),
options = VaultCardExpirationMonth
.entries
.map { it.value() }
.toImmutableList(),
selectedOption = cardState.expirationMonth.value(),
onOptionSelected = { selectedString ->
cardHandlers.onExpirationMonthSelected(
VaultCardExpirationMonth
.entries
.first { it.value.toString(resources) == selectedString },
)
},
modifier = Modifier.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.expiration_year),
value = cardState.expirationYear,
onValueChange = cardHandlers.onExpirationYearTextChange,
keyboardType = KeyboardType.Number,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.security_code),
value = cardState.securityCode,
onValueChange = cardHandlers.onSecurityCodeTextChange,
keyboardType = KeyboardType.NumberPassword,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.miscellaneous),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.folder),
options = commonState.availableFolders.map { it.invoke() }.toImmutableList(),
selectedOption = commonState.folderName.invoke(),
onOptionSelected = commonHandlers.onFolderTextChange,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(
id = R.string.favorite,
),
isChecked = commonState.favorite,
onCheckedChange = commonHandlers.onToggleFavorite,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitchWithActions(
label = stringResource(id = R.string.password_prompt),
isChecked = commonState.masterPasswordReprompt,
onCheckedChange = commonHandlers.onToggleMasterPasswordReprompt,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
actions = {
IconButton(onClick = commonHandlers.onTooltipClick) {
Icon(
painter = painterResource(id = R.drawable.ic_tooltip),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = stringResource(
id = R.string.master_password_re_prompt_help,
),
)
}
},
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = commonState.notes,
onValueChange = commonHandlers.onNotesTextChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
items(commonState.customFieldData) { customItem ->
VaultAddEditCustomField(
customItem,
onCustomFieldValueChange = commonHandlers.onCustomFieldValueChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
supportedLinkedTypes = persistentListOf(
VaultLinkedFieldType.CARDHOLDER_NAME,
VaultLinkedFieldType.EXPIRATION_MONTH,
VaultLinkedFieldType.EXPIRATION_YEAR,
VaultLinkedFieldType.SECURITY_CODE,
VaultLinkedFieldType.BRAND,
VaultLinkedFieldType.NUMBER,
),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
VaultAddEditCustomFieldsButton(
onFinishNamingClick = commonHandlers.onAddNewCustomFieldClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
if (isAddItemMode) {
item {
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ownership),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.who_owns_this_item),
options = commonState.availableOwners.toImmutableList(),
selectedOption = commonState.ownership,
onOptionSelected = commonHandlers.onOwnershipTextChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
}
}

View file

@ -23,6 +23,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitchWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -391,21 +392,21 @@ fun LazyListScope.vaultAddEditIdentityItems(
@Composable
private fun TitleMultiSelectButton(
selectedTitle: VaultAddEditState.ViewState.Content.ItemType.Identity.Title,
onTitleSelected: (VaultAddEditState.ViewState.Content.ItemType.Identity.Title) -> Unit,
selectedTitle: VaultIdentityTitle,
onTitleSelected: (VaultIdentityTitle) -> Unit,
modifier: Modifier = Modifier,
) {
val resources = LocalContext.current.resources
BitwardenMultiSelectButton(
label = stringResource(id = R.string.title),
options = VaultAddEditState.ViewState.Content.ItemType.Identity.Title
options = VaultIdentityTitle
.entries
.map { it.value() }
.toImmutableList(),
selectedOption = selectedTitle.value(),
onOptionSelected = { selectedString ->
onTitleSelected(
VaultAddEditState.ViewState.Content.ItemType.Identity.Title
VaultIdentityTitle
.entries
.first { it.value.toString(resources) == selectedString },
)

View file

@ -15,6 +15,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManager
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
@ -32,6 +33,7 @@ fun VaultAddEditContent(
commonTypeHandlers: VaultAddEditCommonHandlers,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
cardItemTypeHandlers: VaultAddEditCardTypeHandlers,
modifier: Modifier = Modifier,
permissionsManager: PermissionsManager,
) {
@ -39,9 +41,7 @@ fun VaultAddEditContent(
onResult = { isGranted ->
when (state.type) {
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> Unit
// TODO: Create UI for card-type item creation BIT-507
is VaultAddEditState.ViewState.Content.ItemType.Card -> Unit
// TODO: Create UI for identity-type item creation BIT-667
is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Login -> {
loginItemTypeHandlers.onSetupTotpClick(isGranted)
@ -91,7 +91,13 @@ fun VaultAddEditContent(
}
is VaultAddEditState.ViewState.Content.ItemType.Card -> {
// TODO(BIT-507): Create UI for card-type item creation
vaultAddEditCardItems(
commonState = state.common,
cardState = state.type,
commonHandlers = commonTypeHandlers,
cardHandlers = cardItemTypeHandlers,
isAddItemMode = isAddItemMode,
)
}
is VaultAddEditState.ViewState.Content.ItemType.Identity -> {

View file

@ -35,6 +35,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
@ -87,6 +88,10 @@ fun VaultAddEditScreen(
VaultAddEditIdentityTypeHandlers.create(viewModel = viewModel)
}
val cardItemTypeHandlers = remember(viewModel) {
VaultAddEditCardTypeHandlers.create(viewModel = viewModel)
}
VaultAddEditItemDialogs(
dialogState = state.dialog,
onDismissRequest = remember(viewModel) {
@ -131,6 +136,7 @@ fun VaultAddEditScreen(
commonTypeHandlers = commonTypeHandlers,
permissionsManager = permissionsManager,
identityItemTypeHandlers = identityItemTypeHandlers,
cardItemTypeHandlers = cardItemTypeHandlers,
modifier = Modifier
.imePadding()
.padding(innerPadding)

View file

@ -19,6 +19,9 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
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
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
@ -94,6 +97,7 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.Common -> handleCommonActions(action)
is VaultAddEditAction.ItemType.LoginType -> handleAddLoginTypeAction(action)
is VaultAddEditAction.ItemType.IdentityType -> handleIdentityTypeActions(action)
is VaultAddEditAction.ItemType.CardType -> handleCardTypeActions(action)
is VaultAddEditAction.Internal -> handleInternalActions(action)
}
}
@ -157,7 +161,7 @@ class VaultAddEditViewModel @Inject constructor(
private fun handleSwitchToAddCardItem() {
updateContent { currentContent ->
currentContent.copy(
type = VaultAddEditState.ViewState.Content.ItemType.Card,
type = VaultAddEditState.ViewState.Content.ItemType.Card(),
)
}
}
@ -224,11 +228,13 @@ class VaultAddEditViewModel @Inject constructor(
private fun handleAddNewCustomFieldClick(
action: VaultAddEditAction.Common.AddNewCustomFieldClick,
) {
val newCustomData: VaultAddEditState.Custom =
action.customFieldType.toCustomField(action.name)
updateCommonContent { loginType ->
loginType.copy(customFieldData = loginType.customFieldData + newCustomData)
updateCommonContent { common ->
val newCustomData: VaultAddEditState.Custom =
action.customFieldType.toCustomField(action.name)
common.copy(customFieldData = common.customFieldData + newCustomData)
}
}
@ -529,7 +535,7 @@ class VaultAddEditViewModel @Inject constructor(
handleIdentityUsernameTextChange(action)
}
is VaultAddEditAction.ItemType.IdentityType.TitleSelected -> {
is VaultAddEditAction.ItemType.IdentityType.TitleSelect -> {
handleIdentityTitleSelected(action)
}
}
@ -638,12 +644,79 @@ class VaultAddEditViewModel @Inject constructor(
}
private fun handleIdentityTitleSelected(
action: VaultAddEditAction.ItemType.IdentityType.TitleSelected,
action: VaultAddEditAction.ItemType.IdentityType.TitleSelect,
) {
updateIdentityContent { it.copy(selectedTitle = action.title) }
}
//endregion Identity Type Handlers
//region Card Type Handlers
private fun handleCardTypeActions(action: VaultAddEditAction.ItemType.CardType) {
when (action) {
is VaultAddEditAction.ItemType.CardType.BrandSelect -> {
handleCardBrandSelected(action)
}
is VaultAddEditAction.ItemType.CardType.CardHolderNameTextChange -> {
handleCardCardHolderNameTextChange(action)
}
is VaultAddEditAction.ItemType.CardType.ExpirationMonthSelect -> {
handleCardExpirationMonthSelected(action)
}
is VaultAddEditAction.ItemType.CardType.ExpirationYearTextChange -> {
handleCardExpirationYearTextChange(action)
}
is VaultAddEditAction.ItemType.CardType.NumberTextChange -> {
handleCardNumberTextChange(action)
}
is VaultAddEditAction.ItemType.CardType.SecurityCodeTextChange -> {
handleCardSecurityCodeTextChange(action)
}
}
}
private fun handleCardBrandSelected(
action: VaultAddEditAction.ItemType.CardType.BrandSelect,
) {
updateCardContent { it.copy(brand = action.brand) }
}
private fun handleCardCardHolderNameTextChange(
action: VaultAddEditAction.ItemType.CardType.CardHolderNameTextChange,
) {
updateCardContent { it.copy(cardHolderName = action.cardHolderName) }
}
private fun handleCardExpirationMonthSelected(
action: VaultAddEditAction.ItemType.CardType.ExpirationMonthSelect,
) {
updateCardContent { it.copy(expirationMonth = action.expirationMonth) }
}
private fun handleCardExpirationYearTextChange(
action: VaultAddEditAction.ItemType.CardType.ExpirationYearTextChange,
) {
updateCardContent { it.copy(expirationYear = action.expirationYear) }
}
private fun handleCardNumberTextChange(
action: VaultAddEditAction.ItemType.CardType.NumberTextChange,
) {
updateCardContent { it.copy(number = action.number) }
}
private fun handleCardSecurityCodeTextChange(
action: VaultAddEditAction.ItemType.CardType.SecurityCodeTextChange,
) {
updateCardContent { it.copy(securityCode = action.securityCode) }
}
//endregion Card Type Handlers
//region Internal Type Handlers
private fun handleInternalActions(action: VaultAddEditAction.Internal) {
@ -831,6 +904,19 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private inline fun updateCardContent(
crossinline block: (VaultAddEditState.ViewState.Content.ItemType.Card) ->
VaultAddEditState.ViewState.Content.ItemType.Card,
) {
updateContent { currentContent ->
(currentContent.type as? VaultAddEditState.ViewState.Content.ItemType.Card)?.let {
currentContent.copy(
type = block(it),
)
}
}
}
//endregion Utility Functions
}
@ -980,9 +1066,23 @@ data class VaultAddEditState(
/**
* Represents the `Card` item type.
*
* @property cardHolderName The card holder name for the card item.
* @property number The number for the card item.
* @property brand The brand for the card item.
* @property expirationMonth The expiration month for the card item.
* @property expirationYear The expiration year for the card item.
* @property securityCode The security code for the card item.
*/
@Parcelize
data object Card : ItemType() {
data class Card(
val cardHolderName: String = "",
val number: String = "",
val brand: VaultCardBrand = VaultCardBrand.SELECT,
val expirationMonth: VaultCardExpirationMonth = VaultCardExpirationMonth.SELECT,
val expirationYear: String = "",
val securityCode: String = "",
) : ItemType() {
override val displayStringResId: Int get() = ItemTypeOption.CARD.labelRes
}
@ -1010,7 +1110,7 @@ data class VaultAddEditState(
*/
@Parcelize
data class Identity(
val selectedTitle: Title = Title.MR,
val selectedTitle: VaultIdentityTitle = VaultIdentityTitle.MR,
val firstName: String = "",
val middleName: String = "",
val lastName: String = "",
@ -1030,17 +1130,6 @@ data class VaultAddEditState(
val country: String = "",
) : ItemType() {
/**
* Defines all available title options for identities.
*/
enum class Title(val value: Text) {
MR(value = R.string.mr.asText()),
MRS(value = R.string.mrs.asText()),
MS(value = R.string.ms.asText()),
MX(value = R.string.mx.asText()),
DR(value = R.string.dr.asText()),
}
override val displayStringResId: Int get() = ItemTypeOption.IDENTITY.labelRes
}
@ -1450,10 +1539,63 @@ sealed class VaultAddEditAction {
*
* @property title The selected title.
*/
data class TitleSelected(
val title: VaultAddEditState.ViewState.Content.ItemType.Identity.Title,
data class TitleSelect(
val title: VaultIdentityTitle,
) : IdentityType()
}
/**
* Represents actions specific to the Card type.
*/
sealed class CardType : ItemType() {
/**
* Fired when the card holder name text input is changed.
*
* @property cardHolderName The new card holder name text.
*/
data class CardHolderNameTextChange(val cardHolderName: String) : CardType()
/**
* Fired when the number text input is changed.
*
* @property number The new number text.
*/
data class NumberTextChange(val number: String) : CardType()
/**
* Fired when the brand input is selected.
*
* @property brand The selected brand.
*/
data class BrandSelect(
val brand: VaultCardBrand,
) : CardType()
/**
* Fired when the expiration month input is selected.
*
* @property expirationMonth The selected expiration month.
*/
@Suppress("MaxLineLength")
data class ExpirationMonthSelect(
val expirationMonth: VaultCardExpirationMonth,
) : CardType()
/**
* Fired when the expiration year text input is changed.
*
* @property expirationYear The new expiration year text.
*/
data class ExpirationYearTextChange(val expirationYear: String) : CardType()
/**
* Fired when the security code text input is changed.
*
* @property securityCode The new security code text.
*/
data class SecurityCodeTextChange(val securityCode: String) : CardType()
}
}
/**

View file

@ -0,0 +1,81 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
/**
* A collection of handler functions specifically tailored for managing actions
* within the context of adding card items to a vault.
*
* @property onCardHolderNameTextChange Handles the action when the card holder name text is changed.
* @property onNumberTextChange Handles the action when the number text is changed.
* @property onBrandSelected Handles the action when a brand is selected.
* @property onExpirationMonthSelected Handles the action when an expiration month is selected.
* @property onExpirationYearTextChange Handles the action when the expiration year text is changed.
* @property onSecurityCodeTextChange Handles the action when the expiration year text is changed.
*/
@Suppress("MaxLineLength")
data class VaultAddEditCardTypeHandlers(
val onCardHolderNameTextChange: (String) -> Unit,
val onNumberTextChange: (String) -> Unit,
val onBrandSelected: (VaultCardBrand) -> Unit,
val onExpirationMonthSelected: (VaultCardExpirationMonth) -> Unit,
val onExpirationYearTextChange: (String) -> Unit,
val onSecurityCodeTextChange: (String) -> Unit,
) {
companion object {
/**
* Creates an instance of [VaultAddEditCardTypeHandlers] by binding actions
* to the provided [VaultAddEditViewModel].
*/
fun create(viewModel: VaultAddEditViewModel): VaultAddEditCardTypeHandlers =
VaultAddEditCardTypeHandlers(
onCardHolderNameTextChange = { newCardHolderName ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.CardHolderNameTextChange(
cardHolderName = newCardHolderName,
),
)
},
onNumberTextChange = { newNumber ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.NumberTextChange(
number = newNumber,
),
)
},
onBrandSelected = { newBrand ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.BrandSelect(
brand = newBrand,
),
)
},
onExpirationMonthSelected = { newExpirationMonth ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.ExpirationMonthSelect(
expirationMonth = newExpirationMonth,
),
)
},
onExpirationYearTextChange = { newExpirationYear ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.ExpirationYearTextChange(
expirationYear = newExpirationYear,
),
)
},
onSecurityCodeTextChange = { newSecurityCode ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.SecurityCodeTextChange(
securityCode = newSecurityCode,
),
)
},
)
}
}

View file

@ -1,8 +1,8 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
/**
* A collection of handler functions specifically tailored for managing actions
@ -27,7 +27,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
*/
@Suppress("LongParameterList")
data class VaultAddEditIdentityTypeHandlers(
val onTitleSelected: (VaultAddEditState.ViewState.Content.ItemType.Identity.Title) -> Unit,
val onTitleSelected: (VaultIdentityTitle) -> Unit,
val onFirstNameTextChange: (String) -> Unit,
val onMiddleNameTextChange: (String) -> Unit,
val onLastNameTextChange: (String) -> Unit,
@ -57,7 +57,7 @@ data class VaultAddEditIdentityTypeHandlers(
return VaultAddEditIdentityTypeHandlers(
onTitleSelected = { newTitle ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.IdentityType.TitleSelected(
VaultAddEditAction.ItemType.IdentityType.TitleSelect(
title = newTitle,
),
)

View file

@ -8,6 +8,9 @@ import com.bitwarden.core.FieldView
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.VaultCardExpirationMonth
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType.Companion.fromId
import java.util.UUID
@ -28,7 +31,13 @@ fun CipherView.toViewState(): VaultAddEditState.ViewState =
}
CipherType.SECURE_NOTE -> VaultAddEditState.ViewState.Content.ItemType.SecureNotes
CipherType.CARD -> VaultAddEditState.ViewState.Content.ItemType.Card
CipherType.CARD -> VaultAddEditState.ViewState.Content.ItemType.Card(
cardHolderName = card?.cardholderName.orEmpty(),
number = card?.number.orEmpty(),
brand = card?.brand.toBrandOrDefault(),
expirationMonth = card?.expMonth.toExpirationMonthOrDefault(),
securityCode = card?.code.orEmpty(),
)
CipherType.IDENTITY -> VaultAddEditState.ViewState.Content.ItemType.Identity(
selectedTitle = identity?.title.toTitleOrDefault(),
firstName = identity?.firstName.orEmpty(),
@ -93,9 +102,20 @@ private fun FieldView.toCustomField() =
)
}
@Suppress("MaxLineLength")
private fun String?.toTitleOrDefault(): VaultAddEditState.ViewState.Content.ItemType.Identity.Title =
VaultAddEditState.ViewState.Content.ItemType.Identity.Title
private fun String?.toTitleOrDefault(): VaultIdentityTitle =
VaultIdentityTitle
.entries
.find { it.name == this }
?: VaultAddEditState.ViewState.Content.ItemType.Identity.Title.MR
?: VaultIdentityTitle.MR
private fun String?.toBrandOrDefault(): VaultCardBrand =
VaultCardBrand
.entries
.find { it.name == this }
?: VaultCardBrand.SELECT
private fun String?.toExpirationMonthOrDefault(): VaultCardExpirationMonth =
VaultCardExpirationMonth
.entries
.find { it.name == this }
?: VaultCardExpirationMonth.SELECT

View file

@ -0,0 +1,15 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
import com.x8bit.bitwarden.R
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
/**
* Default, "select" Text to show on multi select buttons in the VaultAddEdit package.
*/
val SELECT_TEXT: Text
get() = "-- "
.asText()
.concat(R.string.select.asText())
.concat(" --".asText())

View file

@ -201,7 +201,9 @@ fun VaultItemIdentityContent(
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}

View file

@ -14,10 +14,12 @@ import com.bitwarden.core.SecureNoteView
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
import java.time.Instant
/**
* Transforms a [VaultAddEditState.ViewState.ItemType] into [CipherView].
* Transforms [VaultAddEditState.ViewState.Content] into [CipherView].
*/
fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
CipherView(
@ -64,14 +66,23 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCipherType(): CipherT
private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView? =
(this as? VaultAddEditState.ViewState.Content.ItemType.Card)?.let {
// TODO Create real CardView from Content (BIT-668)
CardView(
cardholderName = null,
expMonth = null,
expYear = null,
code = null,
brand = null,
number = null,
cardholderName = it.cardHolderName.orNullIfBlank(),
expMonth = it
.expirationMonth
.takeUnless { month ->
month == VaultCardExpirationMonth.SELECT
}
?.name,
expYear = it.expirationYear.orNullIfBlank(),
code = it.securityCode.orNullIfBlank(),
brand = it
.brand
.takeUnless { brand ->
brand == VaultCardBrand.SELECT
}
?.name,
number = it.number.orNullIfBlank(),
)
}

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.ui.vault.model
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.SELECT_TEXT
/**
* Defines all available brand options for cards.
*/
enum class VaultCardBrand(val value: Text) {
SELECT(value = SELECT_TEXT),
VISA(value = "Visa".asText()),
MASTERCARD(value = "Mastercard".asText()),
AMERICAN_EXPRESS(value = "American Express".asText()),
DISCOVER(value = "Discover".asText()),
DINERS_CLUB(value = "Diners Club".asText()),
JCB(value = "JCB".asText()),
MAESTRO(value = "Maestro".asText()),
UNIONPAY(value = "UnionPay".asText()),
RUPAY(value = "RuPay".asText()),
OTHER(value = R.string.other.asText()),
}

View file

@ -0,0 +1,34 @@
package com.x8bit.bitwarden.ui.vault.model
import androidx.annotation.StringRes
import com.x8bit.bitwarden.R
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.addedit.util.SELECT_TEXT
/**
* Defines all available expiration month options for cards.
*/
enum class VaultCardExpirationMonth(
val value: Text,
) {
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 - ")),
}
private fun @receiver:StringRes Int.dateText(prefix: String): Text =
prefix
.asText()
.concat(asText())

View file

@ -0,0 +1,16 @@
package com.x8bit.bitwarden.ui.vault.model
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* Defines all available title options for identities.
*/
enum class VaultIdentityTitle(val value: Text) {
MR(value = R.string.mr.asText()),
MRS(value = R.string.mrs.asText()),
MS(value = R.string.ms.asText()),
MX(value = R.string.mx.asText()),
DR(value = R.string.dr.asText()),
}

View file

@ -16,6 +16,13 @@ enum class VaultLinkedFieldType(
) {
USERNAME(id = 100.toUInt(), label = R.string.username.asText()),
PASSWORD(id = 101.toUInt(), label = R.string.password.asText()),
CARDHOLDER_NAME(id = 300.toUInt(), label = R.string.cardholder_name.asText()),
EXPIRATION_MONTH(id = 301.toUInt(), label = R.string.expiration_month.asText()),
EXPIRATION_YEAR(id = 302.toUInt(), label = R.string.expiration_year.asText()),
SECURITY_CODE(id = 303.toUInt(), label = R.string.security_code.asText()),
BRAND(id = 304.toUInt(), label = R.string.brand.asText()),
NUMBER(id = 305.toUInt(), label = R.string.number.asText()),
;
companion object {

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onLast
@ -38,6 +39,9 @@ import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
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 io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -245,7 +249,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
it.copy(
viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
type = VaultAddEditState.ViewState.Content.ItemType.Card,
type = VaultAddEditState.ViewState.Content.ItemType.Card(),
),
)
}
@ -653,8 +657,8 @@ class VaultAddEditScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.IdentityType.TitleSelected(
title = VaultAddEditState.ViewState.Content.ItemType.Identity.Title.MX,
VaultAddEditAction.ItemType.IdentityType.TitleSelect(
title = VaultIdentityTitle.MX,
),
)
}
@ -670,7 +674,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) {
copy(
selectedTitle = VaultAddEditState.ViewState.Content.ItemType.Identity.Title.MX,
selectedTitle = VaultIdentityTitle.MX,
)
}
}
@ -1212,6 +1216,239 @@ class VaultAddEditScreenTest : BaseComposeTest() {
.assertTextContains("NewCountry")
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card changing the card holder name text field should trigger CardHolderNameTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Cardholder name")
.performTextInput(text = "TestCardHolderName")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.CardHolderNameTextChange(
cardHolderName = "TestCardHolderName",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card the card holder name text field should display the text provided by the state`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Cardholder name")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateCardType(currentState) { copy(cardHolderName = "NewCardHolderName") }
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Cardholder name")
.assertTextContains("NewCardHolderName")
}
@Test
fun `in ItemType_Card changing the number text field should trigger NumberTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Number")
.performTextInput(text = "TestNumber")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.NumberTextChange(
number = "TestNumber",
),
)
}
}
@Test
fun `in ItemType_Card the number text field should display the text provided by the state`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Number")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateCardType(currentState) { copy(number = "123") }
}
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Show")
.performClick()
composeTestRule
.onNodeWithTextAfterScroll(text = "Number")
.assertTextContains("123")
}
@Test
fun `in ItemType_Card selecting a brand should trigger BrandSelected`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
// Opens the menu
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Brand, -- Select --")
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "Visa")
.onLast()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.BrandSelect(
brand = VaultCardBrand.VISA,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card the Brand should display the selected brand from the state`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Brand, -- Select --")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateCardType(currentState) {
copy(
brand = VaultCardBrand.AMERICAN_EXPRESS,
)
}
}
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Brand, American Express")
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card selecting an expiration month should trigger ExpirationMonthSelected`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
// Opens the menu
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Expiration month, -- Select --")
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "02 - February")
.onLast()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.ExpirationMonthSelect(
expirationMonth = VaultCardExpirationMonth.FEBRUARY,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card the Expiration month should display the selected expiration month from the state`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Expiration month, -- Select --")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateCardType(currentState) {
copy(
expirationMonth = VaultCardExpirationMonth.MARCH,
)
}
}
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Expiration month, 03 - March")
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card changing the expiration year text field should trigger ExpirationYearTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Expiration year")
.performTextInput(text = "TestExpirationYear")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.ExpirationYearTextChange(
expirationYear = "TestExpirationYear",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card the expiration year text field should display the text provided by the state`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Expiration year")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateCardType(currentState) { copy(expirationYear = "NewExpirationYear") }
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Expiration year")
.assertTextContains("NewExpirationYear")
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card changing the security code text field should trigger SecurityCodeTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Security code")
.performTextInput(text = "TestSecurityCode")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.CardType.SecurityCodeTextChange(
securityCode = "TestSecurityCode",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Card the security code text field should display the text provided by the state`() {
mutableStateFlow.value = DEFAULT_STATE_CARD
composeTestRule
.onNodeWithTextAfterScroll(text = "Security code")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateCardType(currentState) { copy(securityCode = "123") }
}
composeTestRule
.onAllNodesWithContentDescription("Show")
.onLast()
.performClick()
composeTestRule
.onNodeWithTextAfterScroll(text = "Security code")
.assertTextContains("123")
}
@Test
fun `clicking New Custom Field button should allow creation of Linked type`() {
mutableStateFlow.value = DEFAULT_STATE_LOGIN
@ -1754,6 +1991,29 @@ class VaultAddEditScreenTest : BaseComposeTest() {
return currentState.copy(viewState = updatedType)
}
private fun updateCardType(
currentState: VaultAddEditState,
transform: VaultAddEditState.ViewState.Content.ItemType.Card.() ->
VaultAddEditState.ViewState.Content.ItemType.Card,
): VaultAddEditState {
val updatedType = when (val viewState = currentState.viewState) {
is VaultAddEditState.ViewState.Content -> {
when (val type = viewState.type) {
is VaultAddEditState.ViewState.Content.ItemType.Card -> {
viewState.copy(
type = type.transform(),
)
}
else -> viewState
}
}
else -> viewState
}
return currentState.copy(viewState = updatedType)
}
@Suppress("MaxLineLength")
private fun updateCommonContent(
currentState: VaultAddEditState,
@ -1799,6 +2059,15 @@ class VaultAddEditScreenTest : BaseComposeTest() {
dialog = null,
)
private val DEFAULT_STATE_CARD = VaultAddEditState(
vaultAddEditType = VaultAddEditType.AddItem,
viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
type = VaultAddEditState.ViewState.Content.ItemType.Card(),
),
dialog = null,
)
@Suppress("MaxLineLength")
private val DEFAULT_STATE_SECURE_NOTES_CUSTOM_FIELDS = VaultAddEditState(
viewState = VaultAddEditState.ViewState.Content(

View file

@ -16,6 +16,9 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
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
import io.mockk.coEvery
import io.mockk.coVerify
@ -923,13 +926,125 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
@Test
fun `TitleSelected should update title`() = runTest {
val action = VaultAddEditAction.ItemType.IdentityType.TitleSelected(
title = VaultAddEditState.ViewState.Content.ItemType.Identity.Title.MX,
fun `TitleSelect should update title`() = runTest {
val action = VaultAddEditAction.ItemType.IdentityType.TitleSelect(
title = VaultIdentityTitle.MX,
)
val expectedState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Identity(
selectedTitle = VaultAddEditState.ViewState.Content.ItemType.Identity.Title.MX,
selectedTitle = VaultIdentityTitle.MX,
),
)
viewModel.actionChannel.trySend(action)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Nested
inner class VaultAddCardTypeItemActions {
private lateinit var viewModel: VaultAddEditViewModel
private lateinit var vaultAddItemInitialState: VaultAddEditState
private lateinit var identityInitialSavedStateHandle: SavedStateHandle
@BeforeEach
fun setup() {
vaultAddItemInitialState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(),
)
identityInitialSavedStateHandle = createSavedStateHandleWithState(
state = vaultAddItemInitialState,
vaultAddEditType = VaultAddEditType.AddItem,
)
viewModel = createAddVaultItemViewModel(
savedStateHandle = identityInitialSavedStateHandle,
)
}
@Test
fun `CardHolderNameTextChange should update card holder name`() = runTest {
val action = VaultAddEditAction.ItemType.CardType.CardHolderNameTextChange(
cardHolderName = "newCardHolderName",
)
val expectedState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(
cardHolderName = "newCardHolderName",
),
)
viewModel.actionChannel.trySend(action)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `NumberTextChange should update number`() = runTest {
val action = VaultAddEditAction.ItemType.CardType.NumberTextChange(
number = "newNumber",
)
val expectedState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(
number = "newNumber",
),
)
viewModel.actionChannel.trySend(action)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `BrandSelect should update brand`() = runTest {
val action = VaultAddEditAction.ItemType.CardType.BrandSelect(
brand = VaultCardBrand.VISA,
)
val expectedState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(
brand = VaultCardBrand.VISA,
),
)
viewModel.actionChannel.trySend(action)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `ExpirationMonthSelect should update expiration month`() = runTest {
val action = VaultAddEditAction.ItemType.CardType.ExpirationMonthSelect(
expirationMonth = VaultCardExpirationMonth.JUNE,
)
val expectedState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(
expirationMonth = VaultCardExpirationMonth.JUNE,
),
)
viewModel.actionChannel.trySend(action)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `ExpirationYearTextChange should update expiration year`() = runTest {
val action = VaultAddEditAction.ItemType.CardType.ExpirationYearTextChange(
expirationYear = "newExpirationYear",
)
val expectedState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(
expirationYear = "newExpirationYear",
),
)
viewModel.actionChannel.trySend(action)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `SecurityCodeTextChange should update security code`() = runTest {
val action = VaultAddEditAction.ItemType.CardType.SecurityCodeTextChange(
securityCode = "newSecurityCode",
)
val expectedState = createVaultAddItemState(
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(
securityCode = "newSecurityCode",
),
)
viewModel.actionChannel.trySend(action)

View file

@ -68,7 +68,11 @@ class CipherViewExtensionsTest {
availableFolders = emptyList(),
availableOwners = emptyList(),
),
type = VaultAddEditState.ViewState.Content.ItemType.Card,
type = VaultAddEditState.ViewState.Content.ItemType.Card(
cardHolderName = "Bit Warden",
number = "4012888888881881",
securityCode = "123",
),
),
result,
)

View file

@ -14,6 +14,7 @@ import com.bitwarden.core.SecureNoteView
import com.bitwarden.core.UriMatchType
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.VaultIdentityTitle
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import io.mockk.every
import io.mockk.mockkStatic
@ -310,7 +311,7 @@ class VaultAddItemStateExtensionsTest {
ownership = "mockOwnership-1",
),
type = VaultAddEditState.ViewState.Content.ItemType.Identity(
selectedTitle = VaultAddEditState.ViewState.Content.ItemType.Identity.Title.MR,
selectedTitle = VaultIdentityTitle.MR,
firstName = "mockFirstName",
lastName = "mockLastName",
middleName = "mockMiddleName",
@ -407,7 +408,7 @@ class VaultAddItemStateExtensionsTest {
ownership = "mockOwnership-1",
),
type = VaultAddEditState.ViewState.Content.ItemType.Identity(
selectedTitle = VaultAddEditState.ViewState.Content.ItemType.Identity.Title.MR,
selectedTitle = VaultIdentityTitle.MR,
firstName = "mockFirstName",
lastName = "mockLastName",
middleName = "mockMiddleName",