[PM-32810] feat: Add Bank Account item detail view (#6875)

This commit is contained in:
Patrick Honkonen
2026-05-06 16:01:03 +00:00
committed by GitHub
parent 01859beb06
commit 38c4da23bc
12 changed files with 1552 additions and 11 deletions
@@ -0,0 +1,439 @@
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.ui.platform.components.field.BitwardenTextField
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemAttachments
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemCustomFields
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemHistory
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemNotes
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultBankAccountItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.util.longName
/**
* Renders the [VaultItemScreen] content when viewing a bank account cipher.
*/
@Suppress("LongMethod")
@Composable
fun VaultItemBankAccountContent(
commonState: VaultItemState.ViewState.Content.Common,
bankAccountState: VaultItemState.ViewState.Content.ItemType.BankAccount,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
vaultBankAccountItemTypeHandlers: VaultBankAccountItemTypeHandlers,
modifier: Modifier = Modifier,
) {
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
LazyColumn(modifier = modifier.fillMaxWidth()) {
item {
Spacer(Modifier.height(height = 12.dp))
}
itemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
isArchived = commonState.archived,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "BankAccountItemNameIcon",
textFieldTestTag = "BankAccountItemNameEntry",
isExpanded = isExpanded,
onExpandClick = { isExpanded = !isExpanded },
applyIconBackground = commonState.iconData is IconData.Local,
)
item(key = "bankAccountDetailsHeader") {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.details),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
bankAccountState.bankName?.let { bankName ->
item(key = "bankName") {
BitwardenTextField(
label = stringResource(id = BitwardenString.bank_name),
value = bankName,
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "BankAccountItemBankNameEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = bankName),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.nameOnAccount?.let { nameOnAccount ->
item(key = "nameOnAccount") {
BitwardenTextField(
label = stringResource(id = BitwardenString.name_on_account),
value = nameOnAccount,
onValueChange = {},
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_name_on_account,
),
onClick = vaultBankAccountItemTypeHandlers.onCopyNameOnAccountClick,
modifier = Modifier.testTag(
tag = "BankAccountCopyNameOnAccountButton",
),
)
},
textFieldTestTag = "BankAccountItemNameOnAccountEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = nameOnAccount),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.accountType?.let { accountType ->
item(key = "accountType") {
BitwardenTextField(
label = stringResource(id = BitwardenString.account_type),
value = accountType.longName(),
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "BankAccountItemAccountTypeEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = accountType),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.accountNumber?.let { accountNumber ->
item(key = "accountNumber") {
var showAccountNumber by rememberSaveable { mutableStateOf(value = false) }
BitwardenPasswordField(
label = stringResource(id = BitwardenString.account_number),
value = accountNumber,
onValueChange = {},
showPassword = showAccountNumber,
showPasswordChange = { showAccountNumber = it },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_account_number,
),
onClick = vaultBankAccountItemTypeHandlers.onCopyAccountNumberClick,
modifier = Modifier.testTag(
tag = "BankAccountCopyAccountNumberButton",
),
)
},
showPasswordTestTag = "ShowAccountNumberButton",
passwordFieldTestTag = "BankAccountItemAccountNumberEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = accountNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.routingNumber?.let { routingNumber ->
item(key = "routingNumber") {
BitwardenTextField(
label = stringResource(id = BitwardenString.routing_number),
value = routingNumber,
onValueChange = {},
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_routing_number,
),
onClick = vaultBankAccountItemTypeHandlers.onCopyRoutingNumberClick,
modifier = Modifier.testTag(
tag = "BankAccountCopyRoutingNumberButton",
),
)
},
textFieldTestTag = "BankAccountItemRoutingNumberEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = routingNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.branchNumber?.let { branchNumber ->
item(key = "branchNumber") {
BitwardenTextField(
label = stringResource(id = BitwardenString.branch_number),
value = branchNumber,
onValueChange = {},
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_branch_number,
),
onClick = vaultBankAccountItemTypeHandlers.onCopyBranchNumberClick,
modifier = Modifier.testTag(
tag = "BankAccountCopyBranchNumberButton",
),
)
},
textFieldTestTag = "BankAccountItemBranchNumberEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = branchNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.pin?.let { pin ->
item(key = "pin") {
var showPin by rememberSaveable { mutableStateOf(value = false) }
BitwardenPasswordField(
label = stringResource(id = BitwardenString.pin),
value = pin,
onValueChange = {},
showPassword = showPin,
showPasswordChange = { showPin = it },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_pin,
),
onClick = vaultBankAccountItemTypeHandlers.onCopyPinClick,
modifier = Modifier.testTag(tag = "BankAccountCopyPinButton"),
)
},
showPasswordTestTag = "ShowPinButton",
passwordFieldTestTag = "BankAccountItemPinEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = pin),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.swiftCode?.let { swiftCode ->
item(key = "swiftCode") {
BitwardenTextField(
label = stringResource(id = BitwardenString.swift_code),
value = swiftCode,
onValueChange = {},
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_swift_code,
),
onClick = vaultBankAccountItemTypeHandlers.onCopySwiftCodeClick,
modifier = Modifier.testTag(
tag = "BankAccountCopySwiftCodeButton",
),
)
},
textFieldTestTag = "BankAccountItemSwiftCodeEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = swiftCode),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.iban?.let { iban ->
item(key = "iban") {
var showIban by rememberSaveable { mutableStateOf(value = false) }
BitwardenPasswordField(
label = stringResource(id = BitwardenString.iban),
value = iban,
onValueChange = {},
showPassword = showIban,
showPasswordChange = { showIban = it },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_iban,
),
onClick = vaultBankAccountItemTypeHandlers.onCopyIbanClick,
modifier = Modifier.testTag(tag = "BankAccountCopyIbanButton"),
)
},
showPasswordTestTag = "ShowIbanButton",
passwordFieldTestTag = "BankAccountItemIbanEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = iban),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
bankAccountState.bankContactPhone?.let { phone ->
item(key = "bankContactPhone") {
BitwardenTextField(
label = stringResource(id = BitwardenString.bank_contact_phone),
value = phone,
onValueChange = {},
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(
id = BitwardenString.copy_bank_contact_phone,
),
onClick =
vaultBankAccountItemTypeHandlers.onCopyBankContactPhoneClick,
modifier = Modifier.testTag(
tag = "BankAccountCopyBankContactPhoneButton",
),
)
},
textFieldTestTag = "BankAccountItemBankContactPhoneEntry",
cardStyle = bankAccountState
.propertyList
.toListItemCardStyle(
index = bankAccountState.propertyList.indexOf(element = phone),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
vaultItemNotes(
notes = commonState.notes,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
)
vaultItemCustomFields(
customFields = commonState.customFields,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
)
vaultItemAttachments(
attachments = commonState.attachments,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
)
vaultItemHistory(
commonState = commonState,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
loginPasswordRevisionDate = null,
)
item {
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -43,6 +43,7 @@ import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultBankAccountItemTypeHandlers
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.VaultIdentityItemTypeHandlers
@@ -284,6 +285,9 @@ fun VaultItemScreen(
vaultIdentityItemTypeHandlers = remember(viewModel) {
VaultIdentityItemTypeHandlers.create(viewModel = viewModel)
},
vaultBankAccountItemTypeHandlers = remember(viewModel) {
VaultBankAccountItemTypeHandlers.create(viewModel = viewModel)
},
)
}
}
@@ -368,6 +372,7 @@ private fun VaultItemContent(
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
vaultBankAccountItemTypeHandlers: VaultBankAccountItemTypeHandlers,
modifier: Modifier = Modifier,
) {
when (viewState) {
@@ -430,13 +435,23 @@ private fun VaultItemContent(
)
}
is VaultItemState.ViewState.Content.ItemType.BankAccount,
is VaultItemState.ViewState.Content.ItemType.BankAccount -> {
VaultItemBankAccountContent(
commonState = viewState.common,
bankAccountState = viewState.type,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
vaultBankAccountItemTypeHandlers = vaultBankAccountItemTypeHandlers,
modifier = modifier,
)
}
is VaultItemState.ViewState.Content.ItemType.DriversLicense,
is VaultItemState.ViewState.Content.ItemType.Passport,
-> {
// TODO(PM-32810): Render dedicated content for new item types once the UI
// ships in the phase-05-07 PR. Until then these are gated behind the
// pm-32009-new-item-types feature flag and cannot be received.
// TODO(PM-32810): Render dedicated content for the remaining new item types
// once the UI ships in their respective Story slices. Until then these
// branches are gated behind the pm-32009-new-item-types feature flag and
// cannot be received.
VaultItemSecureNoteContent(
commonState = viewState.common,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
@@ -46,6 +46,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections
import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection
import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
@@ -238,6 +239,7 @@ class VaultItemViewModel @Inject constructor(
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action)
is VaultItemAction.ItemType.Identity -> handleIdentityTypeActions(action)
is VaultItemAction.ItemType.BankAccount -> handleBankAccountTypeActions(action)
is VaultItemAction.Common -> handleCommonActions(action)
is VaultItemAction.Internal -> handleInternalAction(action)
}
@@ -1041,6 +1043,134 @@ class VaultItemViewModel @Inject constructor(
//endregion Identity Type Handlers
//region Bank Account Type Handlers
private fun handleBankAccountTypeActions(action: VaultItemAction.ItemType.BankAccount) {
when (action) {
VaultItemAction.ItemType.BankAccount.CopyNameOnAccountClick -> {
handleCopyBankNameOnAccountClick()
}
VaultItemAction.ItemType.BankAccount.CopyAccountNumberClick -> {
handleCopyBankAccountNumberClick()
}
VaultItemAction.ItemType.BankAccount.CopyRoutingNumberClick -> {
handleCopyBankRoutingNumberClick()
}
VaultItemAction.ItemType.BankAccount.CopyBranchNumberClick -> {
handleCopyBankBranchNumberClick()
}
VaultItemAction.ItemType.BankAccount.CopyPinClick -> {
handleCopyBankPinClick()
}
VaultItemAction.ItemType.BankAccount.CopySwiftCodeClick -> {
handleCopyBankSwiftCodeClick()
}
VaultItemAction.ItemType.BankAccount.CopyIbanClick -> {
handleCopyBankIbanClick()
}
VaultItemAction.ItemType.BankAccount.CopyBankContactPhoneClick -> {
handleCopyBankContactPhoneClick()
}
}
}
private fun handleCopyBankNameOnAccountClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.nameOnAccount?.let { nameOnAccount ->
clipboardManager.setText(
text = nameOnAccount,
toastDescriptorOverride = BitwardenString.name_on_account.asText(),
)
}
}
}
private fun handleCopyBankAccountNumberClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.accountNumber?.let { accountNumber ->
clipboardManager.setText(
text = accountNumber,
toastDescriptorOverride = BitwardenString.account_number.asText(),
)
}
}
}
private fun handleCopyBankRoutingNumberClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.routingNumber?.let { routingNumber ->
clipboardManager.setText(
text = routingNumber,
toastDescriptorOverride = BitwardenString.routing_number.asText(),
)
}
}
}
private fun handleCopyBankBranchNumberClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.branchNumber?.let { branchNumber ->
clipboardManager.setText(
text = branchNumber,
toastDescriptorOverride = BitwardenString.branch_number.asText(),
)
}
}
}
private fun handleCopyBankPinClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.pin?.let { pin ->
clipboardManager.setText(
text = pin,
toastDescriptorOverride = BitwardenString.pin.asText(),
)
}
}
}
private fun handleCopyBankSwiftCodeClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.swiftCode?.let { swiftCode ->
clipboardManager.setText(
text = swiftCode,
toastDescriptorOverride = BitwardenString.swift_code.asText(),
)
}
}
}
private fun handleCopyBankIbanClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.iban?.let { iban ->
clipboardManager.setText(
text = iban,
toastDescriptorOverride = BitwardenString.iban.asText(),
)
}
}
}
private fun handleCopyBankContactPhoneClick() {
onBankAccountContent { _, bankAccount ->
bankAccount.bankContactPhone?.let { bankContactPhone ->
clipboardManager.setText(
text = bankContactPhone,
toastDescriptorOverride = BitwardenString.bank_contact_phone.asText(),
)
}
}
}
//endregion Bank Account Type Handlers
//region Internal Type Handlers
private fun handleInternalAction(action: VaultItemAction.Internal) {
@@ -1423,6 +1553,21 @@ class VaultItemViewModel @Inject constructor(
}
}
}
private inline fun onBankAccountContent(
crossinline block: (
VaultItemState.ViewState.Content,
VaultItemState.ViewState.Content.ItemType.BankAccount,
) -> Unit,
) {
state.viewState.asContentOrNull()
?.let { content ->
(content.type as? VaultItemState.ViewState.Content.ItemType.BankAccount)
?.let { bankAccountContent ->
block(content, bankAccountContent)
}
}
}
}
/**
@@ -1472,7 +1617,11 @@ data class VaultItemState(
* Whether the fab is visible.
*/
val isFabVisible: Boolean
get() = viewState is ViewState.Content && !isCipherDeleted && isCipherEditable
get() = viewState is ViewState.Content &&
!isCipherDeleted &&
isCipherEditable &&
// TODO: [PM-32810] Re-enable once Bank Account add/edit is wired.
viewState.asContentOrNull()?.type !is ViewState.Content.ItemType.BankAccount
/**
* Whether the cipher is in a collection.
@@ -1879,7 +2028,7 @@ data class VaultItemState(
data class BankAccount(
val bankName: String?,
val nameOnAccount: String?,
val accountType: String?,
val accountType: VaultBankAccountType?,
val accountNumber: String?,
val routingNumber: String?,
val branchNumber: String?,
@@ -1887,7 +2036,25 @@ data class VaultItemState(
val swiftCode: String?,
val iban: String?,
val bankContactPhone: String?,
) : ItemType()
) : ItemType() {
/**
* An ordered list of Bank Account specific elements.
*/
val propertyList: ImmutableList<Any>
get() = persistentListOfNotNull(
bankName,
nameOnAccount,
accountType,
accountNumber,
routingNumber,
branchNumber,
pin,
swiftCode,
iban,
bankContactPhone,
)
}
/**
* Represents the `DriversLicense` item type.
@@ -2385,6 +2552,52 @@ sealed class VaultItemAction {
*/
data object CopyAddressClick : Identity()
}
/**
* Represents actions specific to the Bank Account type.
*/
sealed class BankAccount : ItemType() {
/**
* The user has clicked the copy button for the name on account.
*/
data object CopyNameOnAccountClick : BankAccount()
/**
* The user has clicked the copy button for the account number.
*/
data object CopyAccountNumberClick : BankAccount()
/**
* The user has clicked the copy button for the routing number.
*/
data object CopyRoutingNumberClick : BankAccount()
/**
* The user has clicked the copy button for the branch number.
*/
data object CopyBranchNumberClick : BankAccount()
/**
* The user has clicked the copy button for the PIN.
*/
data object CopyPinClick : BankAccount()
/**
* The user has clicked the copy button for the SWIFT code.
*/
data object CopySwiftCodeClick : BankAccount()
/**
* The user has clicked the copy button for the IBAN.
*/
data object CopyIbanClick : BankAccount()
/**
* The user has clicked the copy button for the bank contact phone.
*/
data object CopyBankContactPhoneClick : BankAccount()
}
}
/**
@@ -0,0 +1,71 @@
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 bank
* account items in a vault.
*/
data class VaultBankAccountItemTypeHandlers(
val onCopyNameOnAccountClick: () -> Unit,
val onCopyAccountNumberClick: () -> Unit,
val onCopyRoutingNumberClick: () -> Unit,
val onCopyBranchNumberClick: () -> Unit,
val onCopyPinClick: () -> Unit,
val onCopySwiftCodeClick: () -> Unit,
val onCopyIbanClick: () -> Unit,
val onCopyBankContactPhoneClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Creates the [VaultBankAccountItemTypeHandlers] using the [viewModel] to send desired
* actions.
*/
fun create(viewModel: VaultItemViewModel): VaultBankAccountItemTypeHandlers =
VaultBankAccountItemTypeHandlers(
onCopyNameOnAccountClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyNameOnAccountClick,
)
},
onCopyAccountNumberClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyAccountNumberClick,
)
},
onCopyRoutingNumberClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyRoutingNumberClick,
)
},
onCopyBranchNumberClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBranchNumberClick,
)
},
onCopyPinClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyPinClick,
)
},
onCopySwiftCodeClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopySwiftCodeClick,
)
},
onCopyIbanClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyIbanClick,
)
},
onCopyBankContactPhoneClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBankContactPhoneClick,
)
},
)
}
}
@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
@@ -218,7 +219,7 @@ fun CipherView.toViewState(
VaultItemState.ViewState.Content.ItemType.BankAccount(
bankName = bankAccount?.bankName,
nameOnAccount = bankAccount?.nameOnAccount,
accountType = bankAccount?.accountType,
accountType = bankAccount?.accountType?.let(VaultBankAccountType::parse),
accountNumber = bankAccount?.accountNumber,
routingNumber = bankAccount?.routingNumber,
branchNumber = bankAccount?.branchNumber,
@@ -344,7 +345,7 @@ private val CipherType.iconRes: Int
CipherType.IDENTITY -> BitwardenDrawable.ic_id_card
CipherType.SSH_KEY -> BitwardenDrawable.ic_ssh_key
CipherType.LOGIN -> BitwardenDrawable.ic_globe
CipherType.BANK_ACCOUNT -> BitwardenDrawable.ic_note
CipherType.BANK_ACCOUNT -> BitwardenDrawable.ic_payment_card
CipherType.DRIVERS_LICENSE -> BitwardenDrawable.ic_note
CipherType.PASSPORT -> BitwardenDrawable.ic_note
}
@@ -0,0 +1,28 @@
package com.x8bit.bitwarden.ui.vault.util
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.SELECT_TEXT
import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType
/**
* Helper that exposes the display name [Text] for a [VaultBankAccountType].
*/
val VaultBankAccountType.longName: Text
get() = when (this) {
VaultBankAccountType.SELECT -> SELECT_TEXT
VaultBankAccountType.CHECKING -> BitwardenString.checking.asText()
VaultBankAccountType.SAVINGS -> BitwardenString.savings.asText()
VaultBankAccountType.CERTIFICATE_OF_DEPOSIT -> {
BitwardenString.certificate_of_deposit.asText()
}
VaultBankAccountType.LINE_OF_CREDIT -> BitwardenString.line_of_credit.asText()
VaultBankAccountType.INVESTMENT_BROKERAGE -> {
BitwardenString.investment_brokerage.asText()
}
VaultBankAccountType.MONEY_MARKET -> BitwardenString.money_market.asText()
VaultBankAccountType.OTHER -> BitwardenString.other.asText()
}
@@ -45,6 +45,7 @@ import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmen
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
@@ -3252,6 +3253,267 @@ class VaultItemScreenTest : BitwardenComposeTest() {
}
//endregion ssh key
//region bank account
@Test
fun `in bank account state, all fields should be displayed when populated`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll("the bank name").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("the name on account").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("Checking").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("Account number").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("the routing number").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("the branch number").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("PIN").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("the swift code").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("the iban").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("the bank contact phone").assertIsDisplayed()
}
@Test
fun `in bank account state, on copy account number click should send CopyAccountNumberClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy account number")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyAccountNumberClick)
}
}
@Test
fun `in bank account state, on copy routing number click should send CopyRoutingNumberClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy routing number")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyRoutingNumberClick)
}
}
@Test
fun `in bank account state, on copy swift code click should send CopySwiftCodeClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy SWIFT code")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopySwiftCodeClick)
}
}
@Test
fun `in bank account state, on copy iban click should send CopyIbanClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy IBAN")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyIbanClick)
}
}
@Test
fun `in bank account state, bankName should be displayed according to state`() {
val bankName = "the bank name"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(bankName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(bankName = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(bankName)
}
@Test
fun `in bank account state, nameOnAccount should be displayed according to state`() {
val nameOnAccount = "the name on account"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(nameOnAccount).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(nameOnAccount = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(nameOnAccount)
}
@Test
fun `in bank account state, accountType should be displayed according to state`() {
val accountTypeText = "Checking"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(accountTypeText).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(accountType = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(accountTypeText)
}
@Test
fun `in bank account state, accountNumber should be displayed according to state`() {
val accountNumberLabel = "Account number"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(accountNumberLabel).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(accountNumber = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(accountNumberLabel)
}
@Test
fun `in bank account state, routingNumber should be displayed according to state`() {
val routingNumber = "the routing number"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(routingNumber).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(routingNumber = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(routingNumber)
}
@Test
fun `in bank account state, branchNumber should be displayed according to state`() {
val branchNumber = "the branch number"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(branchNumber).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(branchNumber = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(branchNumber)
}
@Test
fun `in bank account state, pin should be displayed according to state`() {
val pinLabel = "PIN"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(pinLabel).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(pin = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(pinLabel)
}
@Test
fun `in bank account state, swiftCode should be displayed according to state`() {
val swiftCode = "the swift code"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(swiftCode).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(swiftCode = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(swiftCode)
}
@Test
fun `in bank account state, iban should be displayed according to state`() {
val iban = "the iban"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(iban).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(iban = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(iban)
}
@Test
fun `in bank account state, bankContactPhone should be displayed according to state`() {
val bankContactPhone = "the bank contact phone"
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(bankContactPhone).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateBankAccountType(currentState) { copy(bankContactPhone = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(bankContactPhone)
}
@Suppress("MaxLineLength")
@Test
fun `in bank account state, on copy name on account click should send CopyNameOnAccountClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy name on account")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyNameOnAccountClick,
)
}
}
@Test
fun `in bank account state, on copy branch number click should send CopyBranchNumberClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy branch number")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBranchNumberClick,
)
}
}
@Test
fun `in bank account state, on copy pin click should send CopyPinClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy PIN")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyPinClick)
}
}
@Suppress("MaxLineLength")
@Test
fun `in bank account state, on copy bank contact phone click should send CopyBankContactPhoneClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy bank contact phone")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBankContactPhoneClick,
)
}
}
@Test
fun `in bank account state, edit fab should not be displayed`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_BANK_ACCOUNT_VIEW_STATE) }
composeTestRule.onNodeWithContentDescription("Edit item").assertDoesNotExist()
}
//endregion bank account
}
//region Helper functions
@@ -3325,6 +3587,29 @@ private fun updateCardType(
return currentState.copy(viewState = updatedType)
}
private fun updateBankAccountType(
currentState: VaultItemState,
transform: VaultItemState.ViewState.Content.ItemType.BankAccount.() ->
VaultItemState.ViewState.Content.ItemType.BankAccount,
): VaultItemState {
val updatedType = when (val viewState = currentState.viewState) {
is VaultItemState.ViewState.Content -> {
when (val type = viewState.type) {
is VaultItemState.ViewState.Content.ItemType.BankAccount -> {
viewState.copy(
type = type.transform(),
)
}
else -> viewState
}
}
else -> viewState
}
return currentState.copy(viewState = updatedType)
}
private fun updateCommonContent(
currentState: VaultItemState,
transform: VaultItemState.ViewState.Content.Common.()
@@ -3468,6 +3753,20 @@ private val DEFAULT_SSH_KEY: VaultItemState.ViewState.Content.ItemType.SshKey =
showPrivateKey = false,
)
private val DEFAULT_BANK_ACCOUNT: VaultItemState.ViewState.Content.ItemType.BankAccount =
VaultItemState.ViewState.Content.ItemType.BankAccount(
bankName = "the bank name",
nameOnAccount = "the name on account",
accountType = VaultBankAccountType.CHECKING,
accountNumber = "the account number",
routingNumber = "the routing number",
branchNumber = "the branch number",
pin = "the pin",
swiftCode = "the swift code",
iban = "the iban",
bankContactPhone = "the bank contact phone",
)
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
name = "cipher",
@@ -3599,6 +3898,12 @@ private val DEFAULT_SSH_KEY_VIEW_STATE: VaultItemState.ViewState.Content =
type = DEFAULT_SSH_KEY,
)
private val DEFAULT_BANK_ACCOUNT_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
common = DEFAULT_COMMON.copy(iconData = IconData.Local(BitwardenDrawable.ic_globe)),
type = DEFAULT_BANK_ACCOUNT,
)
private val EMPTY_VIEW_STATES = listOf(
EMPTY_LOGIN_VIEW_STATE,
EMPTY_IDENTITY_VIEW_STATE,
@@ -54,6 +54,7 @@ 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.feature.verificationcode.util.createVerificationCodeItem
import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
@@ -2602,6 +2603,321 @@ class VaultItemViewModelTest : BaseViewModelTest() {
}
}
@Nested
inner class BankAccountActions {
private lateinit var viewModel: VaultItemViewModel
@BeforeEach
fun setup() {
viewModel = createViewModel(
state = DEFAULT_STATE.copy(viewState = BANK_ACCOUNT_VIEW_STATE),
)
every {
mockCipherView.toViewState(
previousState = null,
isPremiumUser = true,
totpCodeItemData = null,
canDelete = true,
canRestore = false,
canAssignToCollections = true,
canEdit = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
relatedLocations = persistentListOf(),
hasOrganizations = true,
)
} returns BANK_ACCOUNT_VIEW_STATE
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
mutableFoldersStateFlow.value = DataState.Loaded(emptyList())
}
@Test
fun `on CopyAccountNumberClick should copy account number to clipboard`() = runTest {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyAccountNumberClick,
)
verify(exactly = 1) {
clipboardManager.setText(
text = "12345678",
toastDescriptorOverride = BitwardenString.account_number.asText(),
)
}
}
@Test
fun `on CopyRoutingNumberClick should copy routing number to clipboard`() = runTest {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyRoutingNumberClick,
)
verify(exactly = 1) {
clipboardManager.setText(
text = "021000021",
toastDescriptorOverride = BitwardenString.routing_number.asText(),
)
}
}
@Test
fun `on CopySwiftCodeClick should copy SWIFT code to clipboard`() = runTest {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopySwiftCodeClick)
verify(exactly = 1) {
clipboardManager.setText(
text = "BOFAUS3N",
toastDescriptorOverride = BitwardenString.swift_code.asText(),
)
}
}
@Test
fun `on CopyIbanClick should copy IBAN to clipboard`() = runTest {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyIbanClick)
verify(exactly = 1) {
clipboardManager.setText(
text = "GB29NWBK60161331926819",
toastDescriptorOverride = BitwardenString.iban.asText(),
)
}
}
@Test
fun `on CopyNameOnAccountClick should copy name on account to clipboard`() = runTest {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyNameOnAccountClick,
)
verify(exactly = 1) {
clipboardManager.setText(
text = "John Doe",
toastDescriptorOverride = BitwardenString.name_on_account.asText(),
)
}
}
@Test
fun `on CopyBranchNumberClick should copy branch number to clipboard`() = runTest {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBranchNumberClick,
)
verify(exactly = 1) {
clipboardManager.setText(
text = "001",
toastDescriptorOverride = BitwardenString.branch_number.asText(),
)
}
}
@Test
fun `on CopyPinClick should copy pin to clipboard`() = runTest {
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyPinClick)
verify(exactly = 1) {
clipboardManager.setText(
text = "4242",
toastDescriptorOverride = BitwardenString.pin.asText(),
)
}
}
@Test
fun `on CopyBankContactPhoneClick should copy bank contact phone to clipboard`() =
runTest {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBankContactPhoneClick,
)
verify(exactly = 1) {
clipboardManager.setText(
text = "555-0100",
toastDescriptorOverride = BitwardenString.bank_contact_phone.asText(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on CopyAccountNumberClick with null account number should not copy to clipboard`() =
runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(accountNumber = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyAccountNumberClick,
)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on CopyRoutingNumberClick with null routing number should not copy to clipboard`() =
runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(routingNumber = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyRoutingNumberClick,
)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
@Test
fun `on CopySwiftCodeClick with null swift code should not copy to clipboard`() = runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(swiftCode = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopySwiftCodeClick)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
@Test
fun `on CopyIbanClick with null iban should not copy to clipboard`() = runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(iban = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyIbanClick)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on CopyNameOnAccountClick with null name on account should not copy to clipboard`() =
runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(nameOnAccount = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyNameOnAccountClick,
)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on CopyBranchNumberClick with null branch number should not copy to clipboard`() =
runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(branchNumber = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBranchNumberClick,
)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
@Test
fun `on CopyPinClick with null pin should not copy to clipboard`() = runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(pin = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(VaultItemAction.ItemType.BankAccount.CopyPinClick)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on CopyBankContactPhoneClick with null bank contact phone should not copy to clipboard`() =
runTest {
val emptyState = BANK_ACCOUNT_VIEW_STATE.copy(
type = DEFAULT_BANK_ACCOUNT_TYPE.copy(bankContactPhone = null),
)
viewModel = createViewModelWithBankAccountState(emptyState)
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBankContactPhoneClick,
)
verify(exactly = 0) {
clipboardManager.setText(
text = any<String>(),
toastDescriptorOverride = any<Text>(),
)
}
}
private fun createViewModelWithBankAccountState(
viewState: VaultItemState.ViewState.Content,
): VaultItemViewModel {
// Override toViewState to return the requested null-field state.
every {
mockCipherView.toViewState(
previousState = any(),
isPremiumUser = true,
totpCodeItemData = null,
canDelete = true,
canRestore = false,
canAssignToCollections = true,
canEdit = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
relatedLocations = persistentListOf(),
hasOrganizations = true,
)
} returns viewState
val newViewModel = createViewModel(
state = DEFAULT_STATE.copy(viewState = viewState),
)
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
mutableFoldersStateFlow.value = DataState.Loaded(emptyList())
return newViewModel
}
}
@Nested
inner class VaultItemFlow {
@BeforeEach
@@ -3220,5 +3536,26 @@ class VaultItemViewModelTest : BaseViewModelTest() {
common = DEFAULT_COMMON,
type = DEFAULT_IDENTITY_TYPE,
)
private val DEFAULT_BANK_ACCOUNT_TYPE:
VaultItemState.ViewState.Content.ItemType.BankAccount =
VaultItemState.ViewState.Content.ItemType.BankAccount(
bankName = "First National",
nameOnAccount = "John Doe",
accountType = VaultBankAccountType.CHECKING,
accountNumber = "12345678",
routingNumber = "021000021",
branchNumber = "001",
pin = "4242",
swiftCode = "BOFAUS3N",
iban = "GB29NWBK60161331926819",
bankContactPhone = "555-0100",
)
private val BANK_ACCOUNT_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
common = DEFAULT_COMMON,
type = DEFAULT_BANK_ACCOUNT_TYPE,
)
}
}
@@ -0,0 +1,96 @@
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
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
class VaultBankAccountItemTypeHandlersTest {
private val viewModel = mockk<VaultItemViewModel> {
every { trySendAction(any()) } returns Unit
}
private val handlers = VaultBankAccountItemTypeHandlers.create(viewModel = viewModel)
@Test
fun `onCopyNameOnAccountClick should send CopyNameOnAccountClick action`() {
handlers.onCopyNameOnAccountClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyNameOnAccountClick,
)
}
}
@Test
fun `onCopyAccountNumberClick should send CopyAccountNumberClick action`() {
handlers.onCopyAccountNumberClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyAccountNumberClick,
)
}
}
@Test
fun `onCopyRoutingNumberClick should send CopyRoutingNumberClick action`() {
handlers.onCopyRoutingNumberClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyRoutingNumberClick,
)
}
}
@Test
fun `onCopyBranchNumberClick should send CopyBranchNumberClick action`() {
handlers.onCopyBranchNumberClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBranchNumberClick,
)
}
}
@Test
fun `onCopyPinClick should send CopyPinClick action`() {
handlers.onCopyPinClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyPinClick,
)
}
}
@Test
fun `onCopySwiftCodeClick should send CopySwiftCodeClick action`() {
handlers.onCopySwiftCodeClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopySwiftCodeClick,
)
}
}
@Test
fun `onCopyIbanClick should send CopyIbanClick action`() {
handlers.onCopyIbanClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyIbanClick,
)
}
}
@Test
fun `onCopyBankContactPhoneClick should send CopyBankContactPhoneClick action`() {
handlers.onCopyBankContactPhoneClick()
verify(exactly = 1) {
viewModel.trySendAction(
VaultItemAction.ItemType.BankAccount.CopyBankContactPhoneClick,
)
}
}
}
@@ -458,6 +458,7 @@ class CipherViewExtensionsTest {
CipherType.CARD to BitwardenDrawable.ic_payment_card,
CipherType.SECURE_NOTE to BitwardenDrawable.ic_note,
CipherType.SSH_KEY to BitwardenDrawable.ic_ssh_key,
CipherType.BANK_ACCOUNT to BitwardenDrawable.ic_payment_card,
)
.forEach {
val cipherView = createCipherView(type = it.key, isEmpty = false)
@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.vault.util
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.SELECT_TEXT
import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultBankAccountTypeExtensionsTest {
@Test
fun `longName should return the correct value for each VaultBankAccountType`() {
mapOf(
VaultBankAccountType.SELECT to SELECT_TEXT,
VaultBankAccountType.CHECKING to BitwardenString.checking.asText(),
VaultBankAccountType.SAVINGS to BitwardenString.savings.asText(),
VaultBankAccountType.CERTIFICATE_OF_DEPOSIT to
BitwardenString.certificate_of_deposit.asText(),
VaultBankAccountType.LINE_OF_CREDIT to BitwardenString.line_of_credit.asText(),
VaultBankAccountType.INVESTMENT_BROKERAGE to
BitwardenString.investment_brokerage.asText(),
VaultBankAccountType.MONEY_MARKET to BitwardenString.money_market.asText(),
VaultBankAccountType.OTHER to BitwardenString.other.asText(),
)
.forEach { (type, label) ->
assertEquals(label, type.longName)
}
}
}
+7 -2
View File
@@ -1319,15 +1319,19 @@ Do you want to switch to this account?</string>
<string name="name_on_account">Name on account</string>
<string name="account_type">Account type</string>
<string name="account_number">Account number</string>
<string name="routing_number">Routing/transit number</string>
<string name="branch_number">Branch/institution number</string>
<string name="routing_number">Routing number</string>
<string name="branch_number">Branch number</string>
<string name="swift_code">SWIFT code</string>
<string name="iban">IBAN</string>
<string name="bank_contact_phone">Bank contact phone</string>
<string name="copy_name_on_account">Copy name on account</string>
<string name="copy_account_number">Copy account number</string>
<string name="copy_routing_number">Copy routing number</string>
<string name="copy_branch_number">Copy branch number</string>
<string name="copy_pin">Copy PIN</string>
<string name="copy_swift_code">Copy SWIFT code</string>
<string name="copy_iban">Copy IBAN</string>
<string name="copy_bank_contact_phone">Copy bank contact phone</string>
<string name="checking">Checking</string>
<string name="savings">Savings</string>
<string name="certificate_of_deposit">Certificate of deposit</string>
@@ -1345,4 +1349,5 @@ Do you want to switch to this account?</string>
<string name="issuing_authority">Issuing authority/office</string>
<string name="issue_date">Issue date</string>
<string name="expiration_date">Expiration date</string>
<string name="details">Details</string>
</resources>