diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemBankAccountContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemBankAccountContent.kt new file mode 100644 index 0000000000..eef77be59e --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemBankAccountContent.kt @@ -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()) + } + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt index 7a60c09e7b..466a1bbb95 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 9d071936ba..d5ace77d0f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -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 + 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() + } } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultBankAccountItemTypeHandlers.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultBankAccountItemTypeHandlers.kt new file mode 100644 index 0000000000..8510a5a5b1 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultBankAccountItemTypeHandlers.kt @@ -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, + ) + }, + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index f1dba9f17e..1df8b051be 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -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 } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/util/VaultBankAccountTypeExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/util/VaultBankAccountTypeExtensions.kt new file mode 100644 index 0000000000..c548c65de0 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/util/VaultBankAccountTypeExtensions.kt @@ -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() + } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 15cc936121..e83e668ba9 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -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, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 2435d23e16..3edd90887a 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -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(), + toastDescriptorOverride = any(), + ) + } + } + + @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(), + toastDescriptorOverride = any(), + ) + } + } + + @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(), + toastDescriptorOverride = any(), + ) + } + } + + @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(), + toastDescriptorOverride = any(), + ) + } + } + + @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(), + toastDescriptorOverride = any(), + ) + } + } + + @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(), + toastDescriptorOverride = any(), + ) + } + } + + @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(), + toastDescriptorOverride = any(), + ) + } + } + + @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(), + toastDescriptorOverride = any(), + ) + } + } + + 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, + ) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultBankAccountItemTypeHandlersTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultBankAccountItemTypeHandlersTest.kt new file mode 100644 index 0000000000..467b0fddda --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultBankAccountItemTypeHandlersTest.kt @@ -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 { + 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, + ) + } + } +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt index 98bc9872e0..bc877ad27b 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt @@ -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) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/util/VaultBankAccountTypeExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/util/VaultBankAccountTypeExtensionsTest.kt new file mode 100644 index 0000000000..6e767b2005 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/util/VaultBankAccountTypeExtensionsTest.kt @@ -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) + } + } +} diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index e12bb72d26..1499f715c4 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1319,15 +1319,19 @@ Do you want to switch to this account? Name on account Account type Account number - Routing/transit number - Branch/institution number + Routing number + Branch number SWIFT code IBAN Bank contact phone + Copy name on account Copy account number Copy routing number + Copy branch number + Copy PIN Copy SWIFT code Copy IBAN + Copy bank contact phone Checking Savings Certificate of deposit @@ -1345,4 +1349,5 @@ Do you want to switch to this account? Issuing authority/office Issue date Expiration date + Details