From 8223fb30896df6c8a59d74c1efaa79223b3e7d80 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Mon, 4 May 2026 12:40:44 -0400 Subject: [PATCH] [PM-32009] feat: Add infrastructure for new vault item types (#6828) --- .../platform/util/CipherViewExtensions.kt | 3 +- .../util/VaultSdkCipherExtensions.kt | 52 ++++- .../feature/search/SearchViewModel.kt | 2 +- .../search/util/SearchTypeDataExtensions.kt | 2 +- .../components/model/CreateVaultItemType.kt | 15 ++ .../util/CreateVaultItemTypeExtensions.kt | 3 + .../addedit/VaultAddEditItemContent.kt | 7 + .../feature/addedit/VaultAddEditViewModel.kt | 102 +++++++++ .../feature/addedit/model/CustomFieldType.kt | 3 + .../addedit/util/CipherViewExtensions.kt | 17 +- .../addedit/util/VaultAddEditExtensions.kt | 9 + .../ui/vault/feature/item/VaultItemScreen.kt | 14 ++ .../vault/feature/item/VaultItemViewModel.kt | 57 +++++ .../feature/item/util/CipherViewExtensions.kt | 17 +- .../itemlisting/VaultItemListingViewModel.kt | 34 +-- .../util/VaultItemListingDataExtensions.kt | 2 +- .../ui/vault/feature/vault/VaultViewModel.kt | 10 +- .../vault/util/VaultAddItemStateExtensions.kt | 28 ++- .../feature/vault/util/VaultDataExtensions.kt | 3 +- .../ui/vault/model/VaultBankAccountType.kt | 32 +++ .../ui/vault/model/VaultItemCipherType.kt | 15 ++ .../ui/vault/util/CipherTypeExtensions.kt | 2 +- .../datasource/disk/VaultDiskSourceTest.kt | 12 ++ .../datasource/sdk/model/CipherViewUtil.kt | 49 +++-- .../sdk/model/VaultSdkCipherUtil.kt | 14 +- .../util/VaultSdkCipherExtensionsTest.kt | 107 +++++++++- .../feature/search/util/SearchUtil.kt | 35 +++- .../util/CreateVaultItemTypeExtensionsTest.kt | 15 ++ .../addedit/VaultAddEditViewModelTest.kt | 195 +++++++++++++++++ .../VaultItemListingViewModelTest.kt | 51 ++++- .../util/VaultItemListingDataUtil.kt | 44 +++- .../vault/feature/vault/VaultViewModelTest.kt | 31 ++- .../vault/model/VaultBankAccountTypeTest.kt | 66 ++++++ .../core/data/manager/model/FlagKey.kt | 10 + .../core/data/manager/model/FlagKeyTest.kt | 5 + .../network/model/CipherJsonRequest.kt | 9 + .../bitwarden/network/model/CipherTypeJson.kt | 18 ++ .../network/model/LinkedIdTypeJson.kt | 198 ++++++++++++++++++ .../network/model/SyncResponseJson.kt | 174 +++++++++++++++ .../network/service/CiphersServiceTest.kt | 24 +++ .../network/service/SyncServiceTest.kt | 12 ++ .../network/model/CipherJsonRequestUtil.kt | 6 + .../network/model/SyncResponseCipherUtil.kt | 105 ++++++++++ .../components/debug/FeatureFlagListItems.kt | 2 + ui/src/main/res/values/strings.xml | 45 ++++ .../main/res/values/strings_non_localized.xml | 1 + 46 files changed, 1595 insertions(+), 62 deletions(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountType.kt create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountTypeTest.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt index b764e7b6d6..834a706e5a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt @@ -53,9 +53,8 @@ val CipherView.subtitle: String? CipherType.SECURE_NOTE, CipherType.SSH_KEY, + CipherType.BANK_ACCOUNT, -> null - - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt index 8ec70ecb0a..f35dfc2fd7 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt @@ -14,6 +14,7 @@ import com.bitwarden.network.model.SecureNoteTypeJson import com.bitwarden.network.model.SyncResponseJson import com.bitwarden.network.model.UriMatchTypeJson import com.bitwarden.vault.Attachment +import com.bitwarden.vault.BankAccount import com.bitwarden.vault.Card import com.bitwarden.vault.CardListView import com.bitwarden.vault.Cipher @@ -66,6 +67,9 @@ fun Cipher.toEncryptedNetworkCipher( card = card?.toEncryptedNetworkCard(), key = key, sshKey = sshKey?.toEncryptedNetworkSshKey(), + bankAccount = bankAccount?.toEncryptedNetworkBankAccount(), + driversLicense = null, + passport = null, archivedDate = archivedDate, encryptedFor = encryptedFor, ) @@ -96,6 +100,9 @@ fun Cipher.toEncryptedNetworkCipherResponse( card = card?.toEncryptedNetworkCard(), attachments = attachments?.toNetworkAttachmentList(), sshKey = sshKey?.toEncryptedNetworkSshKey(), + bankAccount = bankAccount?.toEncryptedNetworkBankAccount(), + driversLicense = null, + passport = null, shouldOrganizationUseTotp = organizationUseTotp, shouldEdit = edit, revisionDate = revisionDate, @@ -149,6 +156,24 @@ private fun Card.toEncryptedNetworkCard(): SyncResponseJson.Cipher.Card = brand = brand, ) +/** + * Converts a Bitwarden SDK [BankAccount] object to a corresponding + * [SyncResponseJson.Cipher.BankAccount] object. + */ +private fun BankAccount.toEncryptedNetworkBankAccount(): SyncResponseJson.Cipher.BankAccount = + SyncResponseJson.Cipher.BankAccount( + bankName = bankName, + nameOnAccount = nameOnAccount, + accountType = accountType, + accountNumber = accountNumber, + routingNumber = routingNumber, + branchNumber = branchNumber, + pin = pin, + swiftCode = swiftCode, + iban = iban, + bankContactPhone = bankContactPhone, + ) + private fun SshKey.toEncryptedNetworkSshKey(): SyncResponseJson.Cipher.SshKey = SyncResponseJson.Cipher.SshKey( publicKey = publicKey, @@ -373,7 +398,7 @@ private fun CipherType.toNetworkCipherType(): CipherTypeJson = CipherType.CARD -> CipherTypeJson.CARD CipherType.IDENTITY -> CipherTypeJson.IDENTITY CipherType.SSH_KEY -> CipherTypeJson.SSH_KEY - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> CipherTypeJson.BANK_ACCOUNT } /** @@ -401,9 +426,8 @@ fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher = identity = identity?.toSdkIdentity(), sshKey = sshKey?.toSdkSshKey(), card = card?.toSdkCard(), + bankAccount = bankAccount?.toSdkBankAccount(), secureNote = secureNote?.toSdkSecureNote(), - // TODO: PM-32810: Add Bank Account Type - bankAccount = null, favorite = isFavorite, reprompt = reprompt.toSdkRepromptType(), organizationUseTotp = shouldOrganizationUseTotp, @@ -492,6 +516,24 @@ fun SyncResponseJson.Cipher.Card.toSdkCard(): Card = number = number, ) +/** + * Transforms a [SyncResponseJson.Cipher.BankAccount] into the corresponding Bitwarden SDK + * [BankAccount]. + */ +fun SyncResponseJson.Cipher.BankAccount.toSdkBankAccount(): BankAccount = + BankAccount( + bankName = bankName, + nameOnAccount = nameOnAccount, + accountType = accountType, + accountNumber = accountNumber, + routingNumber = routingNumber, + branchNumber = branchNumber, + pin = pin, + swiftCode = swiftCode, + iban = iban, + bankContactPhone = bankContactPhone, + ) + /** * Transforms a [SyncResponseJson.Cipher.SecureNote] into * the corresponding Bitwarden SDK [SecureNote]. @@ -610,6 +652,10 @@ fun CipherTypeJson.toSdkCipherType(): CipherType = CipherTypeJson.CARD -> CipherType.CARD CipherTypeJson.IDENTITY -> CipherType.IDENTITY CipherTypeJson.SSH_KEY -> CipherType.SSH_KEY + CipherTypeJson.BANK_ACCOUNT -> CipherType.BANK_ACCOUNT + CipherTypeJson.DRIVERS_LICENSE, + CipherTypeJson.PASSPORT, + -> throw IllegalArgumentException("SDK mapping not yet available for $this") } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt index 2e676e8a2a..7def40fb67 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt @@ -24,12 +24,12 @@ import com.bitwarden.vault.CipherType import com.bitwarden.vault.CipherView import com.bitwarden.vault.LoginUriView import com.x8bit.bitwarden.data.auth.repository.AuthRepository -import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.util.login +import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt index cfe9da91c2..cf08194d0c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt @@ -266,7 +266,7 @@ private val CipherListViewType.iconRes: Int is CipherListViewType.Card -> BitwardenDrawable.ic_payment_card CipherListViewType.Identity -> BitwardenDrawable.ic_id_card CipherListViewType.SshKey -> BitwardenDrawable.ic_ssh_key - CipherListViewType.BankAccount -> TODO("PM-32810: Add Bank Account Type") + CipherListViewType.BankAccount -> BitwardenDrawable.ic_note } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt index 57d275055d..4898755f2a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt @@ -34,6 +34,21 @@ enum class CreateVaultItemType( */ SSH_KEY(BitwardenString.type_ssh_key), + /** + * A bank account cipher. + */ + BANK_ACCOUNT(BitwardenString.type_bank_account), + + /** + * A driver's license cipher. + */ + DRIVERS_LICENSE(BitwardenString.type_drivers_license), + + /** + * A passport cipher. + */ + PASSPORT(BitwardenString.type_passport), + /** * A cipher item folder */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensions.kt index ca90aca03c..d11b4a44fd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensions.kt @@ -13,5 +13,8 @@ fun CreateVaultItemType.toVaultItemCipherTypeOrNull(): VaultItemCipherType? = wh CreateVaultItemType.IDENTITY -> VaultItemCipherType.IDENTITY CreateVaultItemType.SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE CreateVaultItemType.SSH_KEY -> VaultItemCipherType.SSH_KEY + CreateVaultItemType.BANK_ACCOUNT -> VaultItemCipherType.BANK_ACCOUNT + CreateVaultItemType.DRIVERS_LICENSE -> VaultItemCipherType.DRIVERS_LICENSE + CreateVaultItemType.PASSPORT -> VaultItemCipherType.PASSPORT CreateVaultItemType.FOLDER -> null } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt index 5775f8bf9f..6cf8899cad 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt @@ -73,6 +73,9 @@ fun CoachMarkScope.VaultAddEditContent( is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit is VaultAddEditState.ViewState.Content.ItemType.SshKey -> Unit + is VaultAddEditState.ViewState.Content.ItemType.BankAccount -> Unit + is VaultAddEditState.ViewState.Content.ItemType.DriversLicense -> Unit + is VaultAddEditState.ViewState.Content.ItemType.Passport -> Unit is VaultAddEditState.ViewState.Content.ItemType.Login -> { loginItemTypeHandlers.onSetupTotpClick(isGranted) } @@ -275,6 +278,10 @@ fun CoachMarkScope.VaultAddEditContent( sshKeyTypeHandlers = sshKeyItemTypeHandlers, ) } + + is VaultAddEditState.ViewState.Content.ItemType.BankAccount -> Unit + is VaultAddEditState.ViewState.Content.ItemType.DriversLicense -> Unit + is VaultAddEditState.ViewState.Content.ItemType.Passport -> Unit } vaultAddEditAdditionalOptions( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index 33ab5c62c0..46f776ff2f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -85,6 +85,7 @@ import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType +import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultCollection @@ -434,6 +435,14 @@ class VaultAddEditViewModel @Inject constructor( @Suppress("LongMethod") private fun handleSaveClick() = onContent { content -> + if (!content.type.isSdkSupported) { + sendEvent( + VaultAddEditEvent.ShowSnackbar( + message = BitwardenString.an_error_has_occurred.asText(), + ), + ) + return@onContent + } if (hasValidationErrors(content)) return@onContent mutableStateFlow.update { @@ -2543,6 +2552,9 @@ data class VaultAddEditState( VaultItemCipherType.IDENTITY -> BitwardenString.new_identity.asText() VaultItemCipherType.SECURE_NOTE -> BitwardenString.new_note.asText() VaultItemCipherType.SSH_KEY -> BitwardenString.new_ssh_key.asText() + VaultItemCipherType.BANK_ACCOUNT -> BitwardenString.new_bank_account.asText() + VaultItemCipherType.DRIVERS_LICENSE -> BitwardenString.new_drivers_license.asText() + VaultItemCipherType.PASSPORT -> BitwardenString.new_passport.asText() } is VaultAddEditType.EditItem -> when (cipherType) { @@ -2551,6 +2563,9 @@ data class VaultAddEditState( VaultItemCipherType.IDENTITY -> BitwardenString.edit_identity.asText() VaultItemCipherType.SECURE_NOTE -> BitwardenString.edit_note.asText() VaultItemCipherType.SSH_KEY -> BitwardenString.edit_ssh_key.asText() + VaultItemCipherType.BANK_ACCOUNT -> BitwardenString.edit_bank_account.asText() + VaultItemCipherType.DRIVERS_LICENSE -> BitwardenString.edit_drivers_license.asText() + VaultItemCipherType.PASSPORT -> BitwardenString.edit_passport.asText() } } @@ -2642,6 +2657,9 @@ data class VaultAddEditState( IDENTITY(BitwardenString.type_identity), SECURE_NOTES(BitwardenString.type_secure_note), SSH_KEYS(BitwardenString.type_ssh_key), + BANK_ACCOUNT(BitwardenString.type_bank_account), + DRIVERS_LICENSE(BitwardenString.type_drivers_license), + PASSPORT(BitwardenString.type_passport), } /** @@ -2750,6 +2768,11 @@ data class VaultAddEditState( */ abstract val vaultLinkedFieldTypes: ImmutableList + /** + * Whether this item type has SDK support for save operations. + */ + open val isSdkSupported: Boolean get() = true + /** * Represents the login item information. * @@ -2925,6 +2948,85 @@ data class VaultAddEditState( override val vaultLinkedFieldTypes: ImmutableList get() = persistentListOf() } + + /** + * Represents the bank account item information. + */ + @Parcelize + data class BankAccount( + val bankName: String = "", + val nameOnAccount: String = "", + val accountType: VaultBankAccountType = VaultBankAccountType.SELECT, + val accountNumber: String = "", + val routingNumber: String = "", + val branchNumber: String = "", + val pin: String = "", + val swiftCode: String = "", + val iban: String = "", + val bankContactPhone: String = "", + ) : ItemType() { + override val itemTypeOption: ItemTypeOption + get() = ItemTypeOption.BANK_ACCOUNT + + override val vaultLinkedFieldTypes: ImmutableList + get() = persistentListOf() + } + + /** + * Represents the driver's license item information. + */ + @Parcelize + data class DriversLicense( + val firstName: String = "", + val middleName: String = "", + val lastName: String = "", + val licenseNumber: String = "", + val issuingCountry: String = "", + val issuingState: String = "", + val expirationMonth: String = "", + val expirationDay: String = "", + val expirationYear: String = "", + val licenseClass: String = "", + ) : ItemType() { + override val itemTypeOption: ItemTypeOption + get() = ItemTypeOption.DRIVERS_LICENSE + + override val isSdkSupported: Boolean get() = false + + override val vaultLinkedFieldTypes: ImmutableList + get() = persistentListOf() + } + + /** + * Represents the passport item information. + */ + @Parcelize + data class Passport( + val surname: String = "", + val givenName: String = "", + val dobMonth: String = "", + val dobDay: String = "", + val dobYear: String = "", + val nationality: String = "", + val passportNumber: String = "", + val passportType: String = "", + val issuingCountry: String = "", + val issuingAuthority: String = "", + val issueMonth: String = "", + val issueDay: String = "", + val issueYear: String = "", + val expirationMonth: String = "", + val expirationDay: String = "", + val expirationYear: String = "", + ) : ItemType() { + override val itemTypeOption: ItemTypeOption + get() = ItemTypeOption.PASSPORT + + override val isSdkSupported: Boolean get() = false + + override val vaultLinkedFieldTypes: ImmutableList + get() = persistentListOf() + } } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt index 09c8959cb3..4a07c6511f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt @@ -67,4 +67,7 @@ private val VaultAddEditState.ViewState.Content.ItemType.defaultLinkedFieldTypeO is VaultAddEditState.ViewState.Content.ItemType.Login -> VaultLinkedFieldType.USERNAME is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> null is VaultAddEditState.ViewState.Content.ItemType.SshKey -> null + is VaultAddEditState.ViewState.Content.ItemType.BankAccount -> null + is VaultAddEditState.ViewState.Content.ItemType.DriversLicense -> null + is VaultAddEditState.ViewState.Content.ItemType.Passport -> null } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt index c449b2deb1..e05fb7c435 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt @@ -21,6 +21,7 @@ import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem 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.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultCollection @@ -99,7 +100,18 @@ fun CipherView.toViewState( fingerprint = sshKey?.fingerprint.orEmpty(), ) - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> VaultAddEditState.ViewState.Content.ItemType.BankAccount( + bankName = bankAccount?.bankName.orEmpty(), + nameOnAccount = bankAccount?.nameOnAccount.orEmpty(), + accountType = bankAccount?.accountType.toBankAccountTypeOrDefault(), + accountNumber = bankAccount?.accountNumber.orEmpty(), + routingNumber = bankAccount?.routingNumber.orEmpty(), + branchNumber = bankAccount?.branchNumber.orEmpty(), + pin = bankAccount?.pin.orEmpty(), + swiftCode = bankAccount?.swiftCode.orEmpty(), + iban = bankAccount?.iban.orEmpty(), + bankContactPhone = bankAccount?.bankContactPhone.orEmpty(), + ) }, common = VaultAddEditState.ViewState.Content.Common( originalCipher = this, @@ -331,6 +343,9 @@ private fun String?.toExpirationMonthOrDefault(): VaultCardExpirationMonth = .find { it.number == this } ?: VaultCardExpirationMonth.SELECT +private fun String?.toBankAccountTypeOrDefault(): VaultBankAccountType = + this?.let { VaultBankAccountType.parse(it) } ?: VaultBankAccountType.SELECT + private fun String.appendCloneTextIfRequired( isClone: Boolean, resourceManager: ResourceManager, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt index 5d4fc4968f..5874c8e064 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt @@ -26,4 +26,13 @@ fun VaultItemCipherType.toItemType(): VaultAddEditState.ViewState.Content.ItemTy VaultItemCipherType.IDENTITY -> VaultAddEditState.ViewState.Content.ItemType.Identity() VaultItemCipherType.SECURE_NOTE -> VaultAddEditState.ViewState.Content.ItemType.SecureNotes VaultItemCipherType.SSH_KEY -> VaultAddEditState.ViewState.Content.ItemType.SshKey() + VaultItemCipherType.BANK_ACCOUNT -> { + VaultAddEditState.ViewState.Content.ItemType.BankAccount() + } + VaultItemCipherType.DRIVERS_LICENSE -> { + VaultAddEditState.ViewState.Content.ItemType.DriversLicense() + } + VaultItemCipherType.PASSPORT -> { + VaultAddEditState.ViewState.Content.ItemType.Passport() + } } 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 2c90e12c72..7a60c09e7b 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 @@ -429,6 +429,20 @@ private fun VaultItemContent( modifier = modifier, ) } + + is VaultItemState.ViewState.Content.ItemType.BankAccount, + 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. + VaultItemSecureNoteContent( + commonState = viewState.common, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + modifier = modifier, + ) + } } } 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 1c8aade4a8..7553ccbcd6 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 @@ -1449,6 +1449,9 @@ data class VaultItemState( VaultItemCipherType.IDENTITY -> BitwardenString.view_identity.asText() VaultItemCipherType.SECURE_NOTE -> BitwardenString.view_note.asText() VaultItemCipherType.SSH_KEY -> BitwardenString.view_ssh_key.asText() + VaultItemCipherType.BANK_ACCOUNT -> BitwardenString.view_bank_account.asText() + VaultItemCipherType.DRIVERS_LICENSE -> BitwardenString.view_drivers_license.asText() + VaultItemCipherType.PASSPORT -> BitwardenString.view_passport.asText() } /** @@ -1869,6 +1872,60 @@ data class VaultItemState( val fingerprint: String, val showPrivateKey: Boolean, ) : ItemType() + + /** + * Represents the `BankAccount` item type. + */ + data class BankAccount( + val bankName: String?, + val nameOnAccount: String?, + val accountType: String?, + val accountNumber: String?, + val routingNumber: String?, + val branchNumber: String?, + val pin: String?, + val swiftCode: String?, + val iban: String?, + val bankContactPhone: String?, + ) : ItemType() + + /** + * Represents the `DriversLicense` item type. + */ + data class DriversLicense( + val firstName: String?, + val middleName: String?, + val lastName: String?, + val licenseNumber: String?, + val issuingCountry: String?, + val issuingState: String?, + val expirationMonth: String?, + val expirationDay: String?, + val expirationYear: String?, + val licenseClass: String?, + ) : ItemType() + + /** + * Represents the `Passport` item type. + */ + data class Passport( + val surname: String?, + val givenName: String?, + val dobMonth: String?, + val dobDay: String?, + val dobYear: String?, + val nationality: String?, + val passportNumber: String?, + val passportType: String?, + val issuingCountry: String?, + val issuingAuthority: String?, + val issueMonth: String?, + val issueDay: String?, + val issueYear: String?, + val expirationMonth: String?, + val expirationDay: String?, + val expirationYear: String?, + ) : ItemType() } } 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 043e5a9644..b1e672ebeb 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 @@ -214,7 +214,20 @@ fun CipherView.toViewState( ) } - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> { + VaultItemState.ViewState.Content.ItemType.BankAccount( + bankName = bankAccount?.bankName, + nameOnAccount = bankAccount?.nameOnAccount, + accountType = bankAccount?.accountType, + accountNumber = bankAccount?.accountNumber, + routingNumber = bankAccount?.routingNumber, + branchNumber = bankAccount?.branchNumber, + pin = bankAccount?.pin, + swiftCode = bankAccount?.swiftCode, + iban = bankAccount?.iban, + bankContactPhone = bankAccount?.bankContactPhone, + ) + } }, ) @@ -305,7 +318,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 -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> BitwardenDrawable.ic_note } @get:DrawableRes diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index d480251aeb..9cd07fddd6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -11,9 +11,11 @@ import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderGetCredentialRequest import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.manager.toast.ToastManager import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.util.map +import com.bitwarden.core.util.persistentListOfNotNull import com.bitwarden.data.repository.util.baseIconUrl import com.bitwarden.data.repository.util.baseWebSendUrl import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault @@ -58,6 +60,7 @@ import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParser import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository import com.x8bit.bitwarden.data.credentials.util.getCreatePasskeyCredentialRequestOrNull +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager @@ -152,6 +155,7 @@ class VaultItemListingViewModel @Inject constructor( private val toastManager: ToastManager, private val premiumStateManager: PremiumStateManager, snackbarRelayManager: SnackbarRelayManager, + private val featureFlagManager: FeatureFlagManager, ) : BaseViewModel( initialState = run { val userState = requireNotNull(authRepository.userStateFlow.value) @@ -705,6 +709,9 @@ class VaultItemListingViewModel @Inject constructor( CreateVaultItemType.IDENTITY, CreateVaultItemType.SECURE_NOTE, CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, -> { vaultItemType .toVaultItemCipherTypeOrNull() @@ -841,19 +848,16 @@ class VaultItemListingViewModel @Inject constructor( } private fun createVaultItemTypeSelectionExcludedOptions(): ImmutableList { - // If policy is enable for any organization, exclude the card option - return if (state.restrictItemTypesPolicyOrgIds.isNotEmpty()) { - persistentListOf( - CreateVaultItemType.CARD, - CreateVaultItemType.FOLDER, - CreateVaultItemType.SSH_KEY, - ) - } else { - persistentListOf( - CreateVaultItemType.SSH_KEY, - CreateVaultItemType.FOLDER, - ) - } + val isNewItemTypesEnabled = featureFlagManager + .getFeatureFlag(FlagKey.NewItemTypes) + return persistentListOfNotNull( + CreateVaultItemType.CARD.takeIf { state.restrictItemTypesPolicyOrgIds.isNotEmpty() }, + CreateVaultItemType.FOLDER, + CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT.takeUnless { isNewItemTypesEnabled }, + CreateVaultItemType.DRIVERS_LICENSE.takeUnless { isNewItemTypesEnabled }, + CreateVaultItemType.PASSPORT.takeUnless { isNewItemTypesEnabled }, + ) } private fun handleAddVaultItemClick() { @@ -1339,7 +1343,7 @@ class VaultItemListingViewModel @Inject constructor( CipherType.CARD -> VaultItemCipherType.CARD CipherType.IDENTITY -> VaultItemCipherType.IDENTITY CipherType.SSH_KEY -> VaultItemCipherType.SSH_KEY - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> VaultItemCipherType.BANK_ACCOUNT }, ), ) @@ -1361,7 +1365,7 @@ class VaultItemListingViewModel @Inject constructor( CipherType.CARD -> VaultItemCipherType.CARD CipherType.IDENTITY -> VaultItemCipherType.IDENTITY CipherType.SSH_KEY -> VaultItemCipherType.SSH_KEY - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> VaultItemCipherType.BANK_ACCOUNT }, ), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt index 7fad0ccf9b..33475069dc 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt @@ -588,7 +588,7 @@ private val CipherListViewType.iconRes: Int is CipherListViewType.Card -> BitwardenDrawable.ic_payment_card CipherListViewType.Identity -> BitwardenDrawable.ic_id_card CipherListViewType.SshKey -> BitwardenDrawable.ic_ssh_key - CipherListViewType.BankAccount -> TODO("PM-32810: Add Bank Account Type") + CipherListViewType.BankAccount -> BitwardenDrawable.ic_note } private fun List.applyFilters( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 19bedf5f9d..ada0e12a56 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -117,7 +117,7 @@ class VaultViewModel @Inject constructor( private val browserAutofillDialogManager: BrowserAutofillDialogManager, private val credentialExchangeRegistryManager: CredentialExchangeRegistryManager, private val buildInfoManager: BuildInfoManager, - featureFlagManager: FeatureFlagManager, + private val featureFlagManager: FeatureFlagManager, snackbarRelayManager: SnackbarRelayManager, ) : BaseViewModel( initialState = run { @@ -434,12 +434,17 @@ class VaultViewModel @Inject constructor( } private fun handleSelectAddItemType() { + val isNewItemTypesEnabled = featureFlagManager + .getFeatureFlag(FlagKey.NewItemTypes) // If policy is enable for any organization, exclude the card option val excludedOptions = persistentListOfNotNull( CreateVaultItemType.SSH_KEY, CreateVaultItemType.CARD.takeUnless { state.restrictItemTypesPolicyOrgIds.isEmpty() }, + CreateVaultItemType.BANK_ACCOUNT.takeUnless { isNewItemTypesEnabled }, + CreateVaultItemType.DRIVERS_LICENSE.takeUnless { isNewItemTypesEnabled }, + CreateVaultItemType.PASSPORT.takeUnless { isNewItemTypesEnabled }, ) mutableStateFlow.update { @@ -500,6 +505,9 @@ class VaultViewModel @Inject constructor( CreateVaultItemType.IDENTITY, CreateVaultItemType.SECURE_NOTE, CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, -> { vaultItemType .toVaultItemCipherTypeOrNull() diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt index 908d7267e1..3e75a33d23 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault.util import com.bitwarden.ui.platform.base.util.orNullIfBlank +import com.bitwarden.vault.BankAccountView import com.bitwarden.vault.CardView import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherType @@ -18,6 +19,7 @@ import com.bitwarden.vault.SecureNoteView import com.bitwarden.vault.SshKeyView import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem +import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle @@ -62,8 +64,7 @@ fun VaultAddEditState.ViewState.Content.toCipherView( login = type.toLoginView(common = common, clock = clock), card = type.toCardView(), sshKey = type.toSshKeyView(), - // TODO PM-32810: Add Bank Account Type - bankAccount = null, + bankAccount = type.toBankAccountView(), // Fields we always grab from the UI name = common.name, @@ -82,6 +83,10 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCipherType(): CipherT is VaultAddEditState.ViewState.Content.ItemType.Login -> CipherType.LOGIN is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> CipherType.SECURE_NOTE is VaultAddEditState.ViewState.Content.ItemType.SshKey -> CipherType.SSH_KEY + is VaultAddEditState.ViewState.Content.ItemType.BankAccount -> CipherType.BANK_ACCOUNT + is VaultAddEditState.ViewState.Content.ItemType.DriversLicense, + is VaultAddEditState.ViewState.Content.ItemType.Passport, + -> throw IllegalArgumentException("SDK mapping not yet available for $this") } private fun VaultAddEditState.ViewState.Content.ItemType.toSshKeyView(): SshKeyView? = @@ -93,6 +98,25 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toSshKeyView(): SshKeyV ) } +private fun VaultAddEditState.ViewState.Content.ItemType.toBankAccountView(): BankAccountView? = + (this as? VaultAddEditState.ViewState.Content.ItemType.BankAccount)?.let { + BankAccountView( + bankName = it.bankName.orNullIfBlank(), + nameOnAccount = it.nameOnAccount.orNullIfBlank(), + accountType = it + .accountType + .takeUnless { type -> type == VaultBankAccountType.SELECT } + ?.value, + accountNumber = it.accountNumber.orNullIfBlank(), + routingNumber = it.routingNumber.orNullIfBlank(), + branchNumber = it.branchNumber.orNullIfBlank(), + pin = it.pin.orNullIfBlank(), + swiftCode = it.swiftCode.orNullIfBlank(), + iban = it.iban.orNullIfBlank(), + bankContactPhone = it.bankContactPhone.orNullIfBlank(), + ) + } + private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView? = (this as? VaultAddEditState.ViewState.Content.ItemType.Card)?.let { CardView( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt index 128f1cc99c..af54bdb0fb 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt @@ -365,7 +365,8 @@ private fun CipherListView.toVaultItemOrNull( hasDecryptionError = hasDecryptionError, ) - CipherListViewType.BankAccount -> TODO("PM-32810: Add Bank Account Type") + // TODO: [PM-32009] Map BankAccount to its own VaultItem subclass when the UI is wired. + CipherListViewType.BankAccount -> null } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountType.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountType.kt new file mode 100644 index 0000000000..392faa0ac8 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountType.kt @@ -0,0 +1,32 @@ +package com.x8bit.bitwarden.ui.vault.model + +/** + * Defines all available account type options for bank accounts. The [value] corresponds to the + * string representation used in the server contract, with the exception of [SELECT], which is a + * UI-only placeholder representing no selection. + */ +enum class VaultBankAccountType(val value: String) { + SELECT(value = "select"), + CHECKING(value = "checking"), + SAVINGS(value = "savings"), + CERTIFICATE_OF_DEPOSIT(value = "certificateOfDeposit"), + LINE_OF_CREDIT(value = "lineOfCredit"), + INVESTMENT_BROKERAGE(value = "investmentBrokerage"), + MONEY_MARKET(value = "moneyMarket"), + OTHER(value = "other"), + ; + + /** + * Companion object for [VaultBankAccountType] that exposes parsing utilities. + */ + companion object { + /** + * Returns the [VaultBankAccountType] matching the provided [value] (case-insensitive), + * falling back to [OTHER] if no match is found. + */ + fun parse(value: String?): VaultBankAccountType = + entries + .firstOrNull { it.value.equals(other = value, ignoreCase = true) } + ?: OTHER + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt index e5ab22f1da..18893baa9e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt @@ -32,4 +32,19 @@ enum class VaultItemCipherType { * A SSH key cipher. */ SSH_KEY, + + /** + * A bank account cipher. + */ + BANK_ACCOUNT, + + /** + * A driver's license cipher. + */ + DRIVERS_LICENSE, + + /** + * A passport cipher. + */ + PASSPORT, } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/util/CipherTypeExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/util/CipherTypeExtensions.kt index 0f6ff953b0..7182aad24a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/util/CipherTypeExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/util/CipherTypeExtensions.kt @@ -13,5 +13,5 @@ fun CipherType.toVaultItemCipherType(): VaultItemCipherType = CipherType.CARD -> VaultItemCipherType.CARD CipherType.IDENTITY -> VaultItemCipherType.IDENTITY CipherType.SSH_KEY -> VaultItemCipherType.SSH_KEY - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> VaultItemCipherType.BANK_ACCOUNT } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index 57e448641a..254c74de46 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -589,6 +589,18 @@ private const val CIPHER_JSON = """ "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" }, + "bankAccount": { + "bankName": "mockBankName-1", + "nameOnAccount": "mockNameOnAccount-1", + "accountType": "mockAccountType-1", + "accountNumber": "mockAccountNumber-1", + "routingNumber": "mockRoutingNumber-1", + "branchNumber": "mockBranchNumber-1", + "pin": "mockPin-1", + "swiftCode": "mockSwiftCode-1", + "iban": "mockIban-1", + "bankContactPhone": "mockBankContactPhone-1" + }, "encryptedFor": "mockEncryptedFor-1" } """ diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index 24f14ac17b..736ad61cd6 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -52,7 +52,6 @@ fun createMockCipherView( clock: Clock = FIXED_CLOCK, fido2Credentials: List? = null, sshKey: SshKeyView? = createMockSshKeyView(number = number), - bankAccount: BankAccountView = createMockBankAccountView(number = number), login: LoginView? = createMockLoginView( number = number, password = password, @@ -61,6 +60,7 @@ fun createMockCipherView( fido2Credentials = fido2Credentials, ), card: CardView? = createMockCardView(number = number).takeIf { cipherType == CipherType.CARD }, + bankAccount: BankAccountView? = createMockBankAccountView(number = number), attachments: List = listOf(createMockAttachmentView(number = number)), isArchived: Boolean = false, passwordHistory: List = listOf( @@ -226,6 +226,36 @@ fun createMockCardView( brand = brand, ) +/** + * Create a mock [BankAccountView] with a given [number]. + */ +@Suppress("LongParameterList") +fun createMockBankAccountView( + number: Int, + bankName: String? = "mockBankName-$number", + nameOnAccount: String? = "mockNameOnAccount-$number", + accountType: String? = "checking", + accountNumber: String? = "mockAccountNumber-$number", + routingNumber: String? = "mockRoutingNumber-$number", + branchNumber: String? = "mockBranchNumber-$number", + pin: String? = "mockPin-$number", + swiftCode: String? = "mockSwiftCode-$number", + iban: String? = "mockIban-$number", + bankContactPhone: String? = "mockBankContactPhone-$number", +): BankAccountView = + BankAccountView( + bankName = bankName, + nameOnAccount = nameOnAccount, + accountType = accountType, + accountNumber = accountNumber, + routingNumber = routingNumber, + branchNumber = branchNumber, + pin = pin, + swiftCode = swiftCode, + iban = iban, + bankContactPhone = bankContactPhone, + ) + /** * Create a mock [FieldView] with a given [number]. */ @@ -272,23 +302,6 @@ fun createMockSshKeyView(number: Int): SshKeyView = fingerprint = "mockKeyFingerprint-$number", ) -/** - * Create a mock [BankAccountView] with a given [number]. - */ -fun createMockBankAccountView(number: Int): BankAccountView = - BankAccountView( - bankName = "mockBankName-$number", - nameOnAccount = "mockNameOnAccount-$number", - accountType = "mockAccountType-$number", - accountNumber = "mockAccountNumber-$number", - routingNumber = "mockRoutingNumber-$number", - branchNumber = "mockBranchNumber-$number", - pin = "mockPin-$number", - swiftCode = "mokSwiftCode-$number", - iban = "mockIban-$number", - bankContactPhone = "mockBankContractPhone-$number", - ) - /** * Create a mock [PasswordHistoryView] with a given [number]. */ diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt index 41e59c2c6d..3ca6711fe8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt @@ -33,7 +33,10 @@ private val FIXED_CLOCK: Clock = Clock.fixed( /** * Create a mock [Cipher] with a given [number]. */ -fun createMockSdkCipher(number: Int, clock: Clock = FIXED_CLOCK): Cipher = +fun createMockSdkCipher( + number: Int, + clock: Clock = FIXED_CLOCK, +): Cipher = Cipher( id = "mockId-$number", organizationId = "mockOrganizationId-$number", @@ -50,11 +53,10 @@ fun createMockSdkCipher(number: Int, clock: Clock = FIXED_CLOCK): Cipher = archivedDate = clock.instant(), attachments = listOf(createMockSdkAttachment(number = number)), card = createMockSdkCard(number = number), + bankAccount = createMockSdkBankAccount(number = number), fields = listOf(createMockSdkField(number = number)), identity = createMockSdkIdentity(number = number), sshKey = createMockSdkSshKey(number = number), - // TODO: PM-32810: Add Bank Account Type - bankAccount = null, favorite = false, passwordHistory = listOf(createMockSdkPasswordHistory(number = number, clock = clock)), permissions = createMockSdkCipherPermissions(), @@ -134,7 +136,7 @@ fun createMockSdkSshKey(number: Int): SshKey = /** * Create a mock [BankAccount] with a given [number]. */ -fun createMockBankAccount(number: Int): BankAccount = +fun createMockSdkBankAccount(number: Int): BankAccount = BankAccount( bankName = "mockBankName-$number", nameOnAccount = "mockNameOnAccount-$number", @@ -143,9 +145,9 @@ fun createMockBankAccount(number: Int): BankAccount = routingNumber = "mockRoutingNumber-$number", branchNumber = "mockBranchNumber-$number", pin = "mockPin-$number", - swiftCode = "mokSwiftCode-$number", + swiftCode = "mockSwiftCode-$number", iban = "mockIban-$number", - bankContactPhone = "mockBankContractPhone-$number", + bankContactPhone = "mockBankContactPhone-$number", ) /** diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt index e9430db265..a1dbf15331 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt @@ -6,6 +6,7 @@ import com.bitwarden.network.model.FieldTypeJson import com.bitwarden.network.model.UriMatchTypeJson import com.bitwarden.network.model.createMockAttachment import com.bitwarden.network.model.createMockAttachmentJsonRequest +import com.bitwarden.network.model.createMockBankAccount import com.bitwarden.network.model.createMockCard import com.bitwarden.network.model.createMockCipher import com.bitwarden.network.model.createMockCipherJsonRequest @@ -26,6 +27,7 @@ import com.bitwarden.vault.UriMatchType import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockEncryptionContext import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkBankAccount import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCard import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkField @@ -37,6 +39,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSshKey import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkUri import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Test import org.junit.jupiter.api.assertNull @@ -82,6 +85,8 @@ class VaultSdkCipherExtensionsTest { createMockCipherJsonRequest( number = 1, login = createMockLogin(number = 1, uri = null), + driversLicense = null, + passport = null, archivedDate = FIXED_CLOCK.instant(), ), syncCipher, @@ -175,6 +180,60 @@ class VaultSdkCipherExtensionsTest { ) } + @Test + fun `toSdkBankAccount should convert a SyncResponseJson BankAccount to an SDK BankAccount`() { + val syncBankAccount = createMockBankAccount(number = 1) + val sdkBankAccount = syncBankAccount.toSdkBankAccount() + assertEquals( + createMockSdkBankAccount(number = 1), + sdkBankAccount, + ) + } + + @Test + fun `toSdkBankAccount should pass null account type through to the SDK`() { + val syncBankAccount = createMockBankAccount(number = 1, accountType = null) + val sdkBankAccount = syncBankAccount.toSdkBankAccount() + assertNull(sdkBankAccount.accountType) + } + + @Test + fun `toEncryptedNetworkBankAccount should pass null account type through`() { + val sdkBankAccount = createMockSdkBankAccount(number = 1).copy(accountType = null) + val sdkCipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK) + .copy(bankAccount = sdkBankAccount) + + val networkCipher = sdkCipher.toEncryptedNetworkCipherResponse( + encryptedFor = "mockEncryptedFor-1", + ) + + assertNull(networkCipher.bankAccount?.accountType) + } + + @Suppress("MaxLineLength") + @Test + fun `toEncryptedNetworkCipher with null SDK bankAccount should produce null network bankAccount`() { + val sdkCipher = createMockSdkCipher( + number = 1, + clock = FIXED_CLOCK, + ) + .copy(bankAccount = null) + + val request = sdkCipher.toEncryptedNetworkCipher(encryptedFor = "mockEncryptedFor-1") + + assertNull(request.bankAccount) + } + + @Test + fun `toEncryptedNetworkCipher should always emit null driversLicense and passport`() { + val sdkCipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK) + + val request = sdkCipher.toEncryptedNetworkCipher(encryptedFor = "mockEncryptedFor-1") + + assertNull(request.driversLicense) + assertNull(request.passport) + } + @Test fun `toSdkLoginUriList should convert list of LoginUri to List of Sdk LoginUri`() { val syncLoginUris = listOf( @@ -290,6 +349,31 @@ class VaultSdkCipherExtensionsTest { ) } + @Test + fun `toSdkCipherType should map BANK_ACCOUNT to the SDK BANK_ACCOUNT type`() { + assertEquals( + CipherType.BANK_ACCOUNT, + CipherTypeJson.BANK_ACCOUNT.toSdkCipherType(), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `toSdkCipherType should throw for DRIVERS_LICENSE since SDK mapping is not yet available`() { + val error = assertThrows(IllegalArgumentException::class.java) { + CipherTypeJson.DRIVERS_LICENSE.toSdkCipherType() + } + assertEquals("SDK mapping not yet available for DRIVERS_LICENSE", error.message) + } + + @Test + fun `toSdkCipherType should throw for PASSPORT since SDK mapping is not yet available`() { + val error = assertThrows(IllegalArgumentException::class.java) { + CipherTypeJson.PASSPORT.toSdkCipherType() + } + assertEquals("SDK mapping not yet available for PASSPORT", error.message) + } + @Test fun `toSdkMatchType should convert UriMatchTypeJson to UriMatchType`() { val uriMatchType = UriMatchTypeJson.DOMAIN @@ -358,11 +442,16 @@ class VaultSdkCipherExtensionsTest { @Suppress("MaxLineLength") @Test fun `EncryptionContext toEncryptedNetworkCipher should convert an EncryptionContext to a Network Cipher`() { - val encryptionContext = createMockEncryptionContext(number = 1) + val encryptionContext = createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK), + ) assertEquals( createMockCipherJsonRequest( number = 1, login = createMockLogin(number = 1, uri = null), + driversLicense = null, + passport = null, archivedDate = FIXED_CLOCK.instant(), ), encryptionContext.toEncryptedNetworkCipher(), @@ -372,7 +461,10 @@ class VaultSdkCipherExtensionsTest { @Suppress("MaxLineLength") @Test fun `EncryptionContext toEncryptedNetworkCipherResponse should convert an EncryptionContext to a cipher`() { - val encryptionContext = createMockEncryptionContext(number = 1) + val encryptionContext = createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK), + ) assertEquals( createMockCipher( number = 1, @@ -440,6 +532,17 @@ class VaultSdkCipherExtensionsTest { assertNull(loginType.v1.brand) } + @Suppress("MaxLineLength") + @Test + fun `toFailureCipherListView should convert BankAccount Cipher to CipherListView of type BankAccount`() { + val cipher = createMockSdkCipher(number = 1) + .copy(type = CipherType.BANK_ACCOUNT) + + val result = cipher.toFailureCipherListView() + + assertEquals(CipherListViewType.BankAccount, result.type) + } + @Test fun `updateFromMiniResponse should update cipher with mini response data`() { val originalCipher = createMockCipher( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt index a1d6be4acf..d45080925c 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt @@ -246,7 +246,40 @@ fun createMockDisplayItemForCipher( ) } - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> { + SearchState.DisplayItem( + id = "mockId-$number", + title = "mockName-$number", + titleTestTag = "CipherNameLabel", + subtitle = null, + subtitleTestTag = "CipherSubTitleLabel", + iconData = IconData.Local(BitwardenDrawable.ic_note), + extraIconList = persistentListOf( + IconData.Local( + iconRes = BitwardenDrawable.ic_collections, + contentDescription = BitwardenString.collections.asText(), + testTag = "CipherInCollectionIcon", + ), + ), + overflowOptions = persistentListOf( + ListingItemOverflowAction.VaultAction.ViewClick( + cipherId = "mockId-$number", + cipherType = CipherType.BANK_ACCOUNT, + requiresPasswordReprompt = true, + ), + ListingItemOverflowAction.VaultAction.EditClick( + cipherId = "mockId-$number", + cipherType = CipherType.BANK_ACCOUNT, + requiresPasswordReprompt = true, + ), + ), + overflowTestTag = "CipherOptionsButton", + totpCode = null, + autofillSelectionOptions = persistentListOf(), + shouldDisplayMasterPasswordReprompt = false, + itemType = SearchState.DisplayItem.ItemType.Vault(type = cipherType), + ) + } } /** diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensionsTest.kt index 0bb6b1bebe..1a4bd85ad6 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensionsTest.kt @@ -38,6 +38,21 @@ class CreateVaultItemTypeExtensionsTest { actualResult, ) + CreateVaultItemType.BANK_ACCOUNT -> assertEquals( + VaultItemCipherType.BANK_ACCOUNT, + actualResult, + ) + + CreateVaultItemType.DRIVERS_LICENSE -> assertEquals( + VaultItemCipherType.DRIVERS_LICENSE, + actualResult, + ) + + CreateVaultItemType.PASSPORT -> assertEquals( + VaultItemCipherType.PASSPORT, + actualResult, + ) + CreateVaultItemType.FOLDER -> assertNull(actualResult) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 17e62d60cd..814bfde76b 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -2403,6 +2403,201 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `in add mode, SaveClick with a Drivers License item should emit ShowSnackbar without saving`() = + runTest { + mutableVaultDataFlow.value = DataState.Loaded(createVaultData()) + val driversLicenseState = createVaultAddItemState( + vaultItemCipherType = VaultItemCipherType.DRIVERS_LICENSE, + commonContentViewState = createCommonContentViewState(name = "mockName-1"), + typeContentViewState = + VaultAddEditState.ViewState.Content.ItemType.DriversLicense(), + ) + val viewModel = createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = driversLicenseState, + vaultAddEditType = VaultAddEditType.AddItem, + vaultItemCipherType = VaultItemCipherType.DRIVERS_LICENSE, + ), + ) + + viewModel.eventFlow.test { + viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) + assertEquals( + VaultAddEditEvent.ShowSnackbar( + message = BitwardenString.an_error_has_occurred.asText(), + ), + awaitItem(), + ) + } + assertEquals(driversLicenseState, viewModel.stateFlow.value) + coVerify(exactly = 0) { + vaultRepository.createCipher(any()) + vaultRepository.createCipherInOrganization(any(), any()) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in add mode, SaveClick with a Passport item should emit ShowSnackbar without saving`() = + runTest { + mutableVaultDataFlow.value = DataState.Loaded(createVaultData()) + val passportState = createVaultAddItemState( + vaultItemCipherType = VaultItemCipherType.PASSPORT, + commonContentViewState = createCommonContentViewState(name = "mockName-1"), + typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Passport(), + ) + val viewModel = createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = passportState, + vaultAddEditType = VaultAddEditType.AddItem, + vaultItemCipherType = VaultItemCipherType.PASSPORT, + ), + ) + + viewModel.eventFlow.test { + viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) + assertEquals( + VaultAddEditEvent.ShowSnackbar( + message = BitwardenString.an_error_has_occurred.asText(), + ), + awaitItem(), + ) + } + assertEquals(passportState, viewModel.stateFlow.value) + coVerify(exactly = 0) { + vaultRepository.createCipher(any()) + vaultRepository.createCipherInOrganization(any(), any()) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in add mode, SaveClick with a Bank Account item should not short-circuit and should run validation`() = + runTest { + mutableVaultDataFlow.value = DataState.Loaded(createVaultData()) + val bankAccountState = createVaultAddItemState( + vaultItemCipherType = VaultItemCipherType.BANK_ACCOUNT, + commonContentViewState = createCommonContentViewState(name = ""), + typeContentViewState = + VaultAddEditState.ViewState.Content.ItemType.BankAccount(), + ) + val expectedValidationDialogState = bankAccountState.copy( + dialog = VaultAddEditState.DialogState.Generic( + title = BitwardenString.an_error_has_occurred.asText(), + message = BitwardenString.validation_field_required + .asText(BitwardenString.name.asText()), + ), + ) + val viewModel = createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = bankAccountState, + vaultAddEditType = VaultAddEditType.AddItem, + vaultItemCipherType = VaultItemCipherType.BANK_ACCOUNT, + ), + ) + + viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow -> + viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) + assertEquals(bankAccountState, stateFlow.awaitItem()) + assertEquals(expectedValidationDialogState, stateFlow.awaitItem()) + eventFlow.expectNoEvents() + } + } + + @Test + fun `ItemType BankAccount should expose BANK_ACCOUNT itemTypeOption and be SDK supported`() { + val itemType = VaultAddEditState.ViewState.Content.ItemType.BankAccount() + assertEquals( + VaultAddEditState.ItemTypeOption.BANK_ACCOUNT, + itemType.itemTypeOption, + ) + assertTrue(itemType.isSdkSupported) + assertTrue(itemType.vaultLinkedFieldTypes.isEmpty()) + } + + @Suppress("MaxLineLength") + @Test + fun `ItemType DriversLicense should expose DRIVERS_LICENSE itemTypeOption and not be SDK supported`() { + val itemType = VaultAddEditState.ViewState.Content.ItemType.DriversLicense() + assertEquals( + VaultAddEditState.ItemTypeOption.DRIVERS_LICENSE, + itemType.itemTypeOption, + ) + assertFalse(itemType.isSdkSupported) + assertTrue(itemType.vaultLinkedFieldTypes.isEmpty()) + } + + @Test + fun `ItemType Passport should expose PASSPORT itemTypeOption and not be SDK supported`() { + val itemType = VaultAddEditState.ViewState.Content.ItemType.Passport() + assertEquals( + VaultAddEditState.ItemTypeOption.PASSPORT, + itemType.itemTypeOption, + ) + assertFalse(itemType.isSdkSupported) + assertTrue(itemType.vaultLinkedFieldTypes.isEmpty()) + } + + @Suppress("MaxLineLength") + @Test + fun `screenDisplayName should resolve new title strings for new vault item types in add mode`() { + val baseAddState = createVaultAddItemState(vaultAddEditType = VaultAddEditType.AddItem) + assertEquals( + BitwardenString.new_bank_account.asText(), + baseAddState.copy(cipherType = VaultItemCipherType.BANK_ACCOUNT).screenDisplayName, + ) + assertEquals( + BitwardenString.new_drivers_license.asText(), + baseAddState.copy(cipherType = VaultItemCipherType.DRIVERS_LICENSE).screenDisplayName, + ) + assertEquals( + BitwardenString.new_passport.asText(), + baseAddState.copy(cipherType = VaultItemCipherType.PASSPORT).screenDisplayName, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `screenDisplayName should resolve new title strings for new vault item types in edit mode`() { + val baseEditState = createVaultAddItemState( + vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID), + ) + assertEquals( + BitwardenString.edit_bank_account.asText(), + baseEditState.copy(cipherType = VaultItemCipherType.BANK_ACCOUNT).screenDisplayName, + ) + assertEquals( + BitwardenString.edit_drivers_license.asText(), + baseEditState.copy(cipherType = VaultItemCipherType.DRIVERS_LICENSE).screenDisplayName, + ) + assertEquals( + BitwardenString.edit_passport.asText(), + baseEditState.copy(cipherType = VaultItemCipherType.PASSPORT).screenDisplayName, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `screenDisplayName should resolve new title strings for new vault item types in clone mode`() { + val baseCloneState = createVaultAddItemState( + vaultAddEditType = VaultAddEditType.CloneItem(DEFAULT_EDIT_ITEM_ID), + ) + assertEquals( + BitwardenString.new_bank_account.asText(), + baseCloneState.copy(cipherType = VaultItemCipherType.BANK_ACCOUNT).screenDisplayName, + ) + assertEquals( + BitwardenString.new_drivers_license.asText(), + baseCloneState.copy(cipherType = VaultItemCipherType.DRIVERS_LICENSE).screenDisplayName, + ) + assertEquals( + BitwardenString.new_passport.asText(), + baseCloneState.copy(cipherType = VaultItemCipherType.PASSPORT).screenDisplayName, + ) + } + @Test fun `ArchiveClick without Premium should show ArchiveRequiresPremium dialog`() = runTest { val cipherListView = createMockCipherListView(number = 1, isArchived = false) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 3448d109f0..159f37efce 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -16,6 +16,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.manager.toast.ToastManager import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow @@ -71,6 +72,7 @@ import com.x8bit.bitwarden.data.credentials.model.createMockGetCredentialsReques import com.x8bit.bitwarden.data.credentials.model.createMockProviderGetPasswordCredentialRequest import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParser import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl @@ -300,6 +302,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { private val premiumStateManager: PremiumStateManager = mockk { every { isInAppUpgradeAvailable() } returns false } + private val mutableNewItemTypesFlow = MutableStateFlow(false) + private val featureFlagManager: FeatureFlagManager = mockk { + every { getFeatureFlag(FlagKey.NewItemTypes) } answers { mutableNewItemTypesFlow.value } + } @BeforeEach fun setUp() { @@ -1622,8 +1628,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ), dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( excludedOptions = persistentListOf( - CreateVaultItemType.SSH_KEY, CreateVaultItemType.FOLDER, + CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, ), ), ), @@ -1646,8 +1655,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ), dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( excludedOptions = persistentListOf( - CreateVaultItemType.SSH_KEY, CreateVaultItemType.FOLDER, + CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, ), ), ), @@ -1686,6 +1698,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { CreateVaultItemType.CARD, CreateVaultItemType.FOLDER, CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, ), ), ).copy(restrictItemTypesPolicyOrgIds = persistentListOf("Test Organization")), @@ -1724,6 +1739,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { CreateVaultItemType.CARD, CreateVaultItemType.FOLDER, CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, ), ), ).copy(restrictItemTypesPolicyOrgIds = persistentListOf("Test Organization")), @@ -1731,6 +1749,34 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `AddVaultItemClick should not exclude bank account, drivers license, or passport when NewItemTypes flag is enabled`() { + mutableNewItemTypesFlow.value = true + val viewModel = createVaultItemListingViewModel( + savedStateHandle = createSavedStateHandleWithVaultItemListingType( + vaultItemListingType = VaultItemListingType.Folder(folderId = "id"), + ), + ) + + viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick) + + assertEquals( + createVaultItemListingState( + itemListingType = VaultItemListingState.ItemListingType.Vault.Folder( + folderId = "id", + ), + dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( + excludedOptions = persistentListOf( + CreateVaultItemType.FOLDER, + CreateVaultItemType.SSH_KEY, + ), + ), + ), + viewModel.stateFlow.value, + ) + } + @Test fun `AddVaultItemClick for vault item should emit NavigateToAddVaultItem`() = runTest { val viewModel = createVaultItemListingViewModel() @@ -6192,6 +6238,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { toastManager = toastManager, premiumStateManager = premiumStateManager, relyingPartyParser = relyingPartyParser, + featureFlagManager = featureFlagManager, ) @Suppress("MaxLineLength") diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt index 1b9f88aff8..aed25612fc 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt @@ -277,7 +277,49 @@ fun createMockDisplayItemForCipher( ) } - CipherType.BANK_ACCOUNT -> TODO("PM-32810: Add Bank Account Type") + CipherType.BANK_ACCOUNT -> { + VaultItemListingState.DisplayItem( + id = "mockId-$number", + title = "mockName-$number".asText(), + titleTestTag = "CipherNameLabel", + secondSubtitle = secondSubtitle, + secondSubtitleTestTag = secondSubtitleTestTag, + subtitle = subtitle, + subtitleTestTag = "CipherSubTitleLabel", + iconData = IconData.Local(BitwardenDrawable.ic_note), + extraIconList = persistentListOf( + IconData.Local( + iconRes = BitwardenDrawable.ic_collections, + contentDescription = BitwardenString.collections.asText(), + testTag = "CipherInCollectionIcon", + ), + IconData.Local( + iconRes = BitwardenDrawable.ic_paperclip, + contentDescription = BitwardenString.attachments.asText(), + testTag = "CipherWithAttachmentsIcon", + ), + ), + overflowOptions = listOf( + ListingItemOverflowAction.VaultAction.ViewClick( + cipherId = "mockId-$number", + cipherType = cipherType, + requiresPasswordReprompt = requiresPasswordReprompt, + ), + ListingItemOverflowAction.VaultAction.EditClick( + cipherId = "mockId-$number", + cipherType = cipherType, + requiresPasswordReprompt = requiresPasswordReprompt, + ), + ListingItemOverflowAction.VaultAction.ArchiveClick(cipherId = "mockId-$number"), + ), + optionsTestTag = "CipherOptionsButton", + isAutofill = false, + isCredentialCreation = false, + shouldShowMasterPasswordReprompt = false, + iconTestTag = "BankAccountCipherIcon", + itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = cipherType), + ) + } } /** diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 160644b197..81e7630ed6 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -226,10 +226,12 @@ class VaultViewModelTest : BaseViewModelTest() { coEvery { unregister() } returns UnregisterExportResult.Success } private val mutableCxpExportFeatureFlagFlow = MutableStateFlow(false) + private val mutableNewItemTypesFlagFlow = MutableStateFlow(false) private val featureFlagManager: FeatureFlagManager = mockk { every { getFeatureFlagFlow(FlagKey.CredentialExchangeProtocolExport) } returns mutableCxpExportFeatureFlagFlow + every { getFeatureFlag(FlagKey.NewItemTypes) } answers { mutableNewItemTypesFlagFlow.value } } private val mutablePremiumUpgradeBannerEligibleFlow = MutableStateFlow(false) @@ -3537,7 +3539,12 @@ class VaultViewModelTest : BaseViewModelTest() { viewModel.trySendAction(VaultAction.SelectAddItemType) val expectedState = DEFAULT_STATE.copy( dialog = VaultState.DialogState.SelectVaultAddItemType( - excludedOptions = persistentListOf(CreateVaultItemType.SSH_KEY), + excludedOptions = persistentListOf( + CreateVaultItemType.SSH_KEY, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, + ), ), ) assertEquals( @@ -3569,6 +3576,9 @@ class VaultViewModelTest : BaseViewModelTest() { excludedOptions = persistentListOf( CreateVaultItemType.SSH_KEY, CreateVaultItemType.CARD, + CreateVaultItemType.BANK_ACCOUNT, + CreateVaultItemType.DRIVERS_LICENSE, + CreateVaultItemType.PASSPORT, ), ), restrictItemTypesPolicyOrgIds = listOf("Test Organization"), @@ -3579,6 +3589,25 @@ class VaultViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `SelectAddItemType action should not exclude bank account, drivers license, or passport when NewItemTypes flag is enabled`() { + mutableNewItemTypesFlagFlow.value = true + val viewModel = createViewModel() + + viewModel.trySendAction(VaultAction.SelectAddItemType) + + val expectedState = DEFAULT_STATE.copy( + dialog = VaultState.DialogState.SelectVaultAddItemType( + excludedOptions = persistentListOf(CreateVaultItemType.SSH_KEY), + ), + ) + assertEquals( + expectedState, + viewModel.stateFlow.value, + ) + } + @Test fun `InternetConnectionErrorReceived should show network error if no internet connection`() = runTest { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountTypeTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountTypeTest.kt new file mode 100644 index 0000000000..51063d351a --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountTypeTest.kt @@ -0,0 +1,66 @@ +package com.x8bit.bitwarden.ui.vault.model + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VaultBankAccountTypeTest { + + @Test + fun `each entry should expose the expected server value`() { + val expected = listOf( + VaultBankAccountType.SELECT to "select", + VaultBankAccountType.CHECKING to "checking", + VaultBankAccountType.SAVINGS to "savings", + VaultBankAccountType.CERTIFICATE_OF_DEPOSIT to "certificateOfDeposit", + VaultBankAccountType.LINE_OF_CREDIT to "lineOfCredit", + VaultBankAccountType.INVESTMENT_BROKERAGE to "investmentBrokerage", + VaultBankAccountType.MONEY_MARKET to "moneyMarket", + VaultBankAccountType.OTHER to "other", + ) + + val actual = VaultBankAccountType.entries.map { it to it.value } + + assertEquals(expected, actual) + } + + @Test + fun `parse should return matching entry for exact value`() { + VaultBankAccountType.entries.forEach { entry -> + assertEquals(entry, VaultBankAccountType.parse(entry.value)) + } + } + + @Test + fun `parse should return matching entry regardless of casing`() { + assertEquals( + VaultBankAccountType.CHECKING, + VaultBankAccountType.parse("CHECKING"), + ) + assertEquals( + VaultBankAccountType.CERTIFICATE_OF_DEPOSIT, + VaultBankAccountType.parse("CertificateOfDeposit"), + ) + assertEquals( + VaultBankAccountType.MONEY_MARKET, + VaultBankAccountType.parse("MONEYmarket"), + ) + } + + @Test + fun `parse should fall back to OTHER for null input`() { + assertEquals(VaultBankAccountType.OTHER, VaultBankAccountType.parse(null)) + } + + @Test + fun `parse should fall back to OTHER for an unrecognized value`() { + assertEquals( + VaultBankAccountType.OTHER, + VaultBankAccountType.parse("not-a-real-account-type"), + ) + } + + @Test + fun `parse should fall back to OTHER for a blank value`() { + assertEquals(VaultBankAccountType.OTHER, VaultBankAccountType.parse("")) + } +} diff --git a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt index 419008df24..5fd7bf1b63 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt @@ -43,6 +43,7 @@ sealed class FlagKey { V2EncryptionKeyConnector, V2EncryptionPassword, V2EncryptionTde, + NewItemTypes, ) } } @@ -161,6 +162,15 @@ sealed class FlagKey { override val defaultValue: Boolean = false } + /** + * Data object holding the feature flag key for new vault item types + * (Bank Account, Driver's License, Passport). + */ + data object NewItemTypes : FlagKey() { + override val keyName: String = "pm-32009-new-item-types" + override val defaultValue: Boolean = false + } + //region Dummy keys for testing /** * Data object holding the key for a [Boolean] flag to be used in tests. diff --git a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt index cc5e3b6d69..b071932e06 100644 --- a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt +++ b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt @@ -56,6 +56,10 @@ class FlagKeyTest { FlagKey.V2EncryptionTde.keyName, "pm-27279-v2-registration-tde-jit", ) + assertEquals( + FlagKey.NewItemTypes.keyName, + "pm-32009-new-item-types", + ) } @Test @@ -74,6 +78,7 @@ class FlagKeyTest { FlagKey.V2EncryptionKeyConnector, FlagKey.V2EncryptionPassword, FlagKey.V2EncryptionTde, + FlagKey.NewItemTypes, ).all { !it.defaultValue }, diff --git a/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt b/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt index b82cf0d3c0..1a7bed9c31 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt @@ -56,6 +56,15 @@ data class CipherJsonRequest( @SerialName("sshKey") val sshKey: SyncResponseJson.Cipher.SshKey?, + @SerialName("bankAccount") + val bankAccount: SyncResponseJson.Cipher.BankAccount?, + + @SerialName("driversLicense") + val driversLicense: SyncResponseJson.Cipher.DriversLicense?, + + @SerialName("passport") + val passport: SyncResponseJson.Cipher.Passport?, + @SerialName("folderId") val folderId: String?, diff --git a/network/src/main/kotlin/com/bitwarden/network/model/CipherTypeJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/CipherTypeJson.kt index 0a03a0cc33..5b403a4180 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/CipherTypeJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/CipherTypeJson.kt @@ -39,6 +39,24 @@ enum class CipherTypeJson { */ @SerialName("5") SSH_KEY, + + /** + * A bank account. + */ + @SerialName("6") + BANK_ACCOUNT, + + /** + * A driver's license. + */ + @SerialName("7") + DRIVERS_LICENSE, + + /** + * A passport. + */ + @SerialName("8") + PASSPORT, } @Keep diff --git a/network/src/main/kotlin/com/bitwarden/network/model/LinkedIdTypeJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/LinkedIdTypeJson.kt index 629485ed96..4b89be3e86 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/LinkedIdTypeJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/LinkedIdTypeJson.kt @@ -177,6 +177,204 @@ enum class LinkedIdTypeJson(val value: UInt) { @SerialName("418") IDENTITY_FULL_NAME(value = 418U), // endregion IDENTITY + + // region BANK_ACCOUNT + /** + * The field is linked to the bank account's bank name. + */ + @SerialName("600") + BANK_ACCOUNT_BANK_NAME(value = 600U), + + /** + * The field is linked to the bank account's name on account. + */ + @SerialName("601") + BANK_ACCOUNT_NAME_ON_ACCOUNT(value = 601U), + + /** + * The field is linked to the bank account's account type. + */ + @SerialName("602") + BANK_ACCOUNT_ACCOUNT_TYPE(value = 602U), + + /** + * The field is linked to the bank account's account number. + */ + @SerialName("603") + BANK_ACCOUNT_ACCOUNT_NUMBER(value = 603U), + + /** + * The field is linked to the bank account's routing number. + */ + @SerialName("604") + BANK_ACCOUNT_ROUTING_NUMBER(value = 604U), + + /** + * The field is linked to the bank account's branch number. + */ + @SerialName("605") + BANK_ACCOUNT_BRANCH_NUMBER(value = 605U), + + /** + * The field is linked to the bank account's PIN. + */ + @SerialName("606") + BANK_ACCOUNT_PIN(value = 606U), + + /** + * The field is linked to the bank account's SWIFT code. + */ + @SerialName("607") + BANK_ACCOUNT_SWIFT_CODE(value = 607U), + + /** + * The field is linked to the bank account's IBAN. + */ + @SerialName("608") + BANK_ACCOUNT_IBAN(value = 608U), + + /** + * The field is linked to the bank account's contact phone. + */ + @SerialName("609") + BANK_ACCOUNT_BANK_CONTACT_PHONE(value = 609U), + // endregion BANK_ACCOUNT + + // region DRIVERS_LICENSE + /** + * The field is linked to the driver's license first name. + */ + @SerialName("700") + DRIVERS_LICENSE_FIRST_NAME(value = 700U), + + /** + * The field is linked to the driver's license middle name. + */ + @SerialName("701") + DRIVERS_LICENSE_MIDDLE_NAME(value = 701U), + + /** + * The field is linked to the driver's license last name. + */ + @SerialName("702") + DRIVERS_LICENSE_LAST_NAME(value = 702U), + + /** + * The field is linked to the driver's license number. + */ + @SerialName("703") + DRIVERS_LICENSE_LICENSE_NUMBER(value = 703U), + + /** + * The field is linked to the driver's license issuing country. + */ + @SerialName("704") + DRIVERS_LICENSE_ISSUING_COUNTRY(value = 704U), + + /** + * The field is linked to the driver's license issuing state. + */ + @SerialName("705") + DRIVERS_LICENSE_ISSUING_STATE(value = 705U), + + /** + * The field is linked to the driver's license expiration month. + */ + @SerialName("706") + DRIVERS_LICENSE_EXPIRATION_MONTH(value = 706U), + + /** + * The field is linked to the driver's license expiration year. + */ + @SerialName("707") + DRIVERS_LICENSE_EXPIRATION_YEAR(value = 707U), + + /** + * The field is linked to the driver's license class. + */ + @SerialName("708") + DRIVERS_LICENSE_LICENSE_CLASS(value = 708U), + // endregion DRIVERS_LICENSE + + // region PASSPORT + /** + * The field is linked to the passport surname. + */ + @SerialName("800") + PASSPORT_SURNAME(value = 800U), + + /** + * The field is linked to the passport given name. + */ + @SerialName("801") + PASSPORT_GIVEN_NAME(value = 801U), + + /** + * The field is linked to the passport date of birth month. + */ + @SerialName("802") + PASSPORT_DOB_MONTH(value = 802U), + + /** + * The field is linked to the passport date of birth year. + */ + @SerialName("803") + PASSPORT_DOB_YEAR(value = 803U), + + /** + * The field is linked to the passport nationality. + */ + @SerialName("804") + PASSPORT_NATIONALITY(value = 804U), + + /** + * The field is linked to the passport number. + */ + @SerialName("805") + PASSPORT_PASSPORT_NUMBER(value = 805U), + + /** + * The field is linked to the passport type. + */ + @SerialName("806") + PASSPORT_PASSPORT_TYPE(value = 806U), + + /** + * The field is linked to the passport issuing country. + */ + @SerialName("807") + PASSPORT_ISSUING_COUNTRY(value = 807U), + + /** + * The field is linked to the passport issuing authority. + */ + @SerialName("808") + PASSPORT_ISSUING_AUTHORITY(value = 808U), + + /** + * The field is linked to the passport issue month. + */ + @SerialName("809") + PASSPORT_ISSUE_MONTH(value = 809U), + + /** + * The field is linked to the passport issue year. + */ + @SerialName("810") + PASSPORT_ISSUE_YEAR(value = 810U), + + /** + * The field is linked to the passport expiration month. + */ + @SerialName("811") + PASSPORT_EXPIRATION_MONTH(value = 811U), + + /** + * The field is linked to the passport expiration year. + */ + @SerialName("812") + PASSPORT_EXPIRATION_YEAR(value = 812U), + // endregion PASSPORT } @Keep diff --git a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt index 2b27bb87dd..f0aa7f6d59 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -516,6 +516,15 @@ data class SyncResponseJson( @SerialName("sshKey") val sshKey: SshKey?, + @SerialName("bankAccount") + val bankAccount: BankAccount?, + + @SerialName("driversLicense") + val driversLicense: DriversLicense?, + + @SerialName("passport") + val passport: Passport?, + @SerialName("collectionIds") val collectionIds: List?, @@ -788,6 +797,171 @@ data class SyncResponseJson( val keyFingerprint: String, ) + /** + * Represents a bank account in the vault response. + * + * @property bankName The name of the bank (nullable). + * @property nameOnAccount The name on the account (nullable). + * @property accountType The type of bank account (nullable). + * @property accountNumber The account number (nullable). + * @property routingNumber The routing/transit number (nullable). + * @property branchNumber The branch/institution number (nullable). + * @property pin The PIN (nullable). + * @property swiftCode The SWIFT code (nullable). + * @property iban The IBAN (nullable). + * @property bankContactPhone The bank contact phone number (nullable). + */ + @Serializable + data class BankAccount( + @SerialName("bankName") + val bankName: String?, + + @SerialName("nameOnAccount") + val nameOnAccount: String?, + + @SerialName("accountType") + val accountType: String?, + + @SerialName("accountNumber") + val accountNumber: String?, + + @SerialName("routingNumber") + val routingNumber: String?, + + @SerialName("branchNumber") + val branchNumber: String?, + + @SerialName("pin") + val pin: String?, + + @SerialName("swiftCode") + val swiftCode: String?, + + @SerialName("iban") + val iban: String?, + + @SerialName("bankContactPhone") + val bankContactPhone: String?, + ) + + /** + * Represents a driver's license in the vault response. + * + * @property firstName The first name (nullable). + * @property middleName The middle name (nullable). + * @property lastName The last name (nullable). + * @property licenseNumber The license number (nullable). + * @property issuingCountry The issuing country (nullable). + * @property issuingState The issuing state/province (nullable). + * @property expirationMonth The expiration month (nullable). + * @property expirationDay The expiration day of month (nullable). + * @property expirationYear The expiration year (nullable). + * @property licenseClass The license class (nullable). + */ + @Serializable + data class DriversLicense( + @SerialName("firstName") + val firstName: String?, + + @SerialName("middleName") + val middleName: String?, + + @SerialName("lastName") + val lastName: String?, + + @SerialName("licenseNumber") + val licenseNumber: String?, + + @SerialName("issuingCountry") + val issuingCountry: String?, + + @SerialName("issuingState") + val issuingState: String?, + + @SerialName("expirationMonth") + val expirationMonth: String?, + + @SerialName("expirationDay") + val expirationDay: String?, + + @SerialName("expirationYear") + val expirationYear: String?, + + @SerialName("licenseClass") + val licenseClass: String?, + ) + + /** + * Represents a passport in the vault response. + * + * @property surname The surname (nullable). + * @property givenName The given name (nullable). + * @property dobMonth The month of birth (nullable). + * @property dobDay The day of month of birth (nullable). + * @property dobYear The year of birth (nullable). + * @property nationality The nationality (nullable). + * @property passportNumber The passport number (nullable). + * @property passportType The passport type (nullable). + * @property issuingCountry The issuing country (nullable). + * @property issuingAuthority The issuing authority/office (nullable). + * @property issueMonth The issue month (nullable). + * @property issueDay The issue day of month (nullable). + * @property issueYear The issue year (nullable). + * @property expirationMonth The expiration month (nullable). + * @property expirationDay The expiration day of month (nullable). + * @property expirationYear The expiration year (nullable). + */ + @Serializable + data class Passport( + @SerialName("surname") + val surname: String?, + + @SerialName("givenName") + val givenName: String?, + + @SerialName("dobMonth") + val dobMonth: String?, + + @SerialName("dobDay") + val dobDay: String?, + + @SerialName("dobYear") + val dobYear: String?, + + @SerialName("nationality") + val nationality: String?, + + @SerialName("passportNumber") + val passportNumber: String?, + + @SerialName("passportType") + val passportType: String?, + + @SerialName("issuingCountry") + val issuingCountry: String?, + + @SerialName("issuingAuthority") + val issuingAuthority: String?, + + @SerialName("issueMonth") + val issueMonth: String?, + + @SerialName("issueDay") + val issueDay: String?, + + @SerialName("issueYear") + val issueYear: String?, + + @SerialName("expirationMonth") + val expirationMonth: String?, + + @SerialName("expirationDay") + val expirationDay: String?, + + @SerialName("expirationYear") + val expirationYear: String?, + ) + /** * Represents password history in the vault response. * diff --git a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt index 41353d72cb..454b021e94 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt @@ -629,6 +629,18 @@ private const val CREATE_ATTACHMENT_SUCCESS_JSON = """ "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" }, + "bankAccount": { + "bankName": "mockBankName-1", + "nameOnAccount": "mockNameOnAccount-1", + "accountType": "mockAccountType-1", + "accountNumber": "mockAccountNumber-1", + "routingNumber": "mockRoutingNumber-1", + "branchNumber": "mockBranchNumber-1", + "pin": "mockPin-1", + "swiftCode": "mockSwiftCode-1", + "iban": "mockIban-1", + "bankContactPhone": "mockBankContactPhone-1" + }, "encryptedFor": "mockEncryptedFor-1", "archivedDate": "2023-10-27T12:00:00.00Z" } @@ -758,6 +770,18 @@ private const val CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON = """ "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" }, + "bankAccount": { + "bankName": "mockBankName-1", + "nameOnAccount": "mockNameOnAccount-1", + "accountType": "mockAccountType-1", + "accountNumber": "mockAccountNumber-1", + "routingNumber": "mockRoutingNumber-1", + "branchNumber": "mockBranchNumber-1", + "pin": "mockPin-1", + "swiftCode": "mockSwiftCode-1", + "iban": "mockIban-1", + "bankContactPhone": "mockBankContactPhone-1" + }, "encryptedFor": "mockEncryptedFor-1", "archivedDate": "2023-10-27T12:00:00.00Z" } diff --git a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt index 0e28169717..aea7c433b1 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt @@ -335,6 +335,18 @@ private const val SYNC_SUCCESS_JSON = """ "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" }, + "bankAccount": { + "bankName": "mockBankName-1", + "nameOnAccount": "mockNameOnAccount-1", + "accountType": "mockAccountType-1", + "accountNumber": "mockAccountNumber-1", + "routingNumber": "mockRoutingNumber-1", + "branchNumber": "mockBranchNumber-1", + "pin": "mockPin-1", + "swiftCode": "mockSwiftCode-1", + "iban": "mockIban-1", + "bankContactPhone": "mockBankContactPhone-1" + }, "encryptedFor": "mockEncryptedFor-1", "archivedDate": "2023-10-27T12:00:00.00Z" } diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt index 847e8bd9fd..0330ee2e11 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt @@ -19,6 +19,9 @@ fun createMockCipherJsonRequest( login: SyncResponseJson.Cipher.Login? = createMockLogin(number = number), card: SyncResponseJson.Cipher.Card? = createMockCard(number = number), sshKey: SyncResponseJson.Cipher.SshKey? = createMockSshKey(number = number), + bankAccount: SyncResponseJson.Cipher.BankAccount? = createMockBankAccount(number = number), + driversLicense: SyncResponseJson.Cipher.DriversLicense? = null, + passport: SyncResponseJson.Cipher.Passport? = null, identity: SyncResponseJson.Cipher.Identity? = createMockIdentity(number = number), secureNote: SyncResponseJson.Cipher.SecureNote? = createMockSecureNote(), fields: List? = listOf(createMockField(number = number)), @@ -42,6 +45,9 @@ fun createMockCipherJsonRequest( login = login, card = card, sshKey = sshKey, + bankAccount = bankAccount, + driversLicense = driversLicense, + passport = passport, identity = identity, secureNote = secureNote, fields = fields, diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt index 3bfe9c9ea0..7c9bfbc3e4 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt @@ -32,6 +32,9 @@ fun createMockCipher( card: SyncResponseJson.Cipher.Card? = createMockCard(number = number), identity: SyncResponseJson.Cipher.Identity? = createMockIdentity(number = number), sshKey: SyncResponseJson.Cipher.SshKey? = createMockSshKey(number = number), + bankAccount: SyncResponseJson.Cipher.BankAccount? = createMockBankAccount(number = number), + driversLicense: SyncResponseJson.Cipher.DriversLicense? = null, + passport: SyncResponseJson.Cipher.Passport? = null, secureNote: SyncResponseJson.Cipher.SecureNote? = createMockSecureNote(), fields: List? = listOf(createMockField(number = number)), isFavorite: Boolean = false, @@ -59,6 +62,9 @@ fun createMockCipher( identity = identity, secureNote = secureNote, sshKey = sshKey, + bankAccount = bankAccount, + driversLicense = driversLicense, + passport = passport, creationDate = creationDate, revisionDate = revisionDate, deletedDate = deletedDate, @@ -257,6 +263,105 @@ fun createMockSshKey( keyFingerprint = keyFingerprint, ) +/** + * Create a mock [SyncResponseJson.Cipher.BankAccount] with a given [number]. + */ +fun createMockBankAccount( + number: Int, + bankName: String? = "mockBankName-$number", + nameOnAccount: String? = "mockNameOnAccount-$number", + accountType: String? = "mockAccountType-$number", + accountNumber: String? = "mockAccountNumber-$number", + routingNumber: String? = "mockRoutingNumber-$number", + branchNumber: String? = "mockBranchNumber-$number", + pin: String? = "mockPin-$number", + swiftCode: String? = "mockSwiftCode-$number", + iban: String? = "mockIban-$number", + bankContactPhone: String? = "mockBankContactPhone-$number", +): SyncResponseJson.Cipher.BankAccount = + SyncResponseJson.Cipher.BankAccount( + bankName = bankName, + nameOnAccount = nameOnAccount, + accountType = accountType, + accountNumber = accountNumber, + routingNumber = routingNumber, + branchNumber = branchNumber, + pin = pin, + swiftCode = swiftCode, + iban = iban, + bankContactPhone = bankContactPhone, + ) + +/** + * Create a mock [SyncResponseJson.Cipher.DriversLicense] with a given [number]. + */ +fun createMockDriversLicense( + number: Int, + firstName: String? = "mockFirstName-$number", + middleName: String? = "mockMiddleName-$number", + lastName: String? = "mockLastName-$number", + licenseNumber: String? = "mockLicenseNumber-$number", + issuingCountry: String? = "mockIssuingCountry-$number", + issuingState: String? = "mockIssuingState-$number", + expirationMonth: String? = "mockExpirationMonth-$number", + expirationDay: String? = "mockExpirationDay-$number", + expirationYear: String? = "mockExpirationYear-$number", + licenseClass: String? = "mockLicenseClass-$number", +): SyncResponseJson.Cipher.DriversLicense = + SyncResponseJson.Cipher.DriversLicense( + firstName = firstName, + middleName = middleName, + lastName = lastName, + licenseNumber = licenseNumber, + issuingCountry = issuingCountry, + issuingState = issuingState, + expirationMonth = expirationMonth, + expirationDay = expirationDay, + expirationYear = expirationYear, + licenseClass = licenseClass, + ) + +/** + * Create a mock [SyncResponseJson.Cipher.Passport] with a given [number]. + */ +fun createMockPassport( + number: Int, + surname: String? = "mockSurname-$number", + givenName: String? = "mockGivenName-$number", + dobMonth: String? = "mockDobMonth-$number", + dobDay: String? = "mockDobDay-$number", + dobYear: String? = "mockDobYear-$number", + nationality: String? = "mockNationality-$number", + passportNumber: String? = "mockPassportNumber-$number", + passportType: String? = "mockPassportType-$number", + issuingCountry: String? = "mockIssuingCountry-$number", + issuingAuthority: String? = "mockIssuingAuthority-$number", + issueMonth: String? = "mockIssueMonth-$number", + issueDay: String? = "mockIssueDay-$number", + issueYear: String? = "mockIssueYear-$number", + expirationMonth: String? = "mockExpirationMonth-$number", + expirationDay: String? = "mockExpirationDay-$number", + expirationYear: String? = "mockExpirationYear-$number", +): SyncResponseJson.Cipher.Passport = + SyncResponseJson.Cipher.Passport( + surname = surname, + givenName = givenName, + dobMonth = dobMonth, + dobDay = dobDay, + dobYear = dobYear, + nationality = nationality, + passportNumber = passportNumber, + passportType = passportType, + issuingCountry = issuingCountry, + issuingAuthority = issuingAuthority, + issueMonth = issueMonth, + issueDay = issueDay, + issueYear = issueYear, + expirationMonth = expirationMonth, + expirationDay = expirationDay, + expirationYear = expirationYear, + ) + /** * Create a mock [SyncResponseJson.Cipher.Fido2Credential] with a given [number]. */ diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt index 6dad716fa4..b462fc680a 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt @@ -37,6 +37,7 @@ fun FlagKey.ListItemContent( FlagKey.V2EncryptionKeyConnector, FlagKey.V2EncryptionPassword, FlagKey.V2EncryptionTde, + FlagKey.NewItemTypes, -> { @Suppress("UNCHECKED_CAST") BooleanFlagItem( @@ -95,4 +96,5 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.V2EncryptionKeyConnector -> stringResource(BitwardenString.v2_encryption_key_connector) FlagKey.V2EncryptionPassword -> stringResource(BitwardenString.v2_encryption_password) FlagKey.V2EncryptionTde -> stringResource(BitwardenString.v2_encryption_tde) + FlagKey.NewItemTypes -> stringResource(BitwardenString.new_item_types) } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index df16800469..a10aaf5a0b 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1295,4 +1295,49 @@ Do you want to switch to this account? We had trouble loading the management portal, so try again. Subscription error We couldn’t load your subscription details. Please try again. + View bank account + View driver’s license + View passport + New bank account + New driver’s license + New passport + Edit bank account + Edit driver’s license + Edit passport + Bank account + Driver’s license + Passport + Bank accounts + Driver’s licenses + Passports + Bank name + Name on account + Account type + Account number + Routing/transit number + Branch/institution number + SWIFT code + IBAN + Bank contact phone + Copy account number + Copy routing number + Copy SWIFT code + Copy IBAN + Checking + Savings + Certificate of deposit + Line of credit + Investment/brokerage + Money market + Issuing country + Issuing state/province + License class + Surname + Given name + Date of birth + Nationality + Passport type + Issuing authority/office + Issue date + Expiration date diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index dcfc884182..bf07d081e1 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -50,6 +50,7 @@ V2 Encryption - Key Connector V2 Encryption - JIT Password V2 Encryption - Password + New Item Types