From 0ad22d2fc29c2a04d16345cf95d5534197e1e7ed Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 7 Apr 2026 19:14:10 -0400 Subject: [PATCH 1/2] [PM-32009] feat: Add infrastructure for new vault item types Register pm-32009-new-item-types feature flag and add foundational support for Bank Account, Driver's License, and Passport cipher types across the network model, SDK mapping, and UI layers. Network: CipherTypeJson enum values (6-8), SyncResponseJson nested data classes, CipherJsonRequest fields, LinkedIdTypeJson entries (600-812). SDK: Defensive mapping that gracefully skips unsupported cipher types during sync decryption (SDK types not yet available). UI: VaultBankAccountType enum, VaultItemCipherType/CreateVaultItemType extensions, ItemType state classes in VaultAddEditViewModel, string resources, and exhaustive when-branch updates across ViewModels. --- .../util/VaultSdkCipherExtensions.kt | 14 +- .../components/model/CreateVaultItemType.kt | 15 ++ .../util/CreateVaultItemTypeExtensions.kt | 3 + .../addedit/VaultAddEditItemContent.kt | 7 + .../feature/addedit/VaultAddEditViewModel.kt | 81 +++++++ .../feature/addedit/model/CustomFieldType.kt | 3 + .../addedit/util/VaultAddEditExtensions.kt | 6 + .../vault/feature/item/VaultItemViewModel.kt | 3 + .../itemlisting/VaultItemListingViewModel.kt | 3 + .../ui/vault/feature/vault/VaultViewModel.kt | 3 + .../vault/util/VaultAddItemStateExtensions.kt | 4 + .../ui/vault/model/VaultBankAccountType.kt | 31 +++ .../ui/vault/model/VaultItemCipherType.kt | 15 ++ .../util/CreateVaultItemTypeExtensionsTest.kt | 15 ++ .../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 | 158 ++++++++++++++ .../network/model/CipherJsonRequestUtil.kt | 6 + .../network/model/SyncResponseCipherUtil.kt | 97 +++++++++ .../components/debug/FeatureFlagListItems.kt | 2 + ui/src/main/res/values/strings.xml | 45 ++++ .../main/res/values/strings_non_localized.xml | 1 + 25 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountType.kt 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 e787dcf5ce0..d2531f8708e 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 @@ -66,6 +66,9 @@ fun Cipher.toEncryptedNetworkCipher( card = card?.toEncryptedNetworkCard(), key = key, sshKey = sshKey?.toEncryptedNetworkSshKey(), + bankAccount = null, + driversLicense = null, + passport = null, archivedDate = archivedDate, encryptedFor = encryptedFor, ) @@ -96,6 +99,9 @@ fun Cipher.toEncryptedNetworkCipherResponse( card = card?.toEncryptedNetworkCard(), attachments = attachments?.toNetworkAttachmentList(), sshKey = sshKey?.toEncryptedNetworkSshKey(), + bankAccount = null, + driversLicense = null, + passport = null, shouldOrganizationUseTotp = organizationUseTotp, shouldEdit = edit, revisionDate = revisionDate, @@ -380,7 +386,9 @@ private fun CipherType.toNetworkCipherType(): CipherTypeJson = * Bitwarden SDK [Cipher] objects. */ fun List.toEncryptedSdkCipherList(): List = - map { it.toEncryptedSdkCipher() } + mapNotNull { + runCatching { it.toEncryptedSdkCipher() }.getOrNull() + } /** * Converts a [SyncResponseJson.Cipher] object to a corresponding @@ -607,6 +615,10 @@ fun CipherTypeJson.toSdkCipherType(): CipherType = CipherTypeJson.CARD -> CipherType.CARD CipherTypeJson.IDENTITY -> CipherType.IDENTITY CipherTypeJson.SSH_KEY -> CipherType.SSH_KEY + CipherTypeJson.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/vault/components/model/CreateVaultItemType.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt index 57d275055d0..4898755f2a2 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 ca90aca03c3..d11b4a44fd4 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 5775f8bf9f2..6cf8899cada 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 65ec60f9bc5..3b9322cd870 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 @@ -89,6 +89,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultCollection import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType +import com.x8bit.bitwarden.ui.vault.model.VaultBankAccountType import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import com.x8bit.bitwarden.ui.vault.util.detectCardBrand import dagger.hilt.android.lifecycle.HiltViewModel @@ -2552,6 +2553,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) { @@ -2560,6 +2564,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() } } @@ -2653,6 +2660,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), } /** @@ -2936,6 +2946,77 @@ 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 expirationYear: String = "", + val licenseClass: String = "", + ) : ItemType() { + override val itemTypeOption: ItemTypeOption + get() = ItemTypeOption.DRIVERS_LICENSE + + 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 dobYear: String = "", + val nationality: String = "", + val passportNumber: String = "", + val passportType: String = "", + val issuingCountry: String = "", + val issuingAuthority: String = "", + val issueMonth: String = "", + val issueYear: String = "", + val expirationMonth: String = "", + val expirationYear: String = "", + ) : ItemType() { + override val itemTypeOption: ItemTypeOption + get() = ItemTypeOption.PASSPORT + + 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 09c8959cb3e..4a07c6511ff 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/VaultAddEditExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt index 5d4fc4968f0..a16f50e7f4b 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,10 @@ 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/VaultItemViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index a0bb049de2a..abc713977f7 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 @@ -1460,6 +1460,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() } /** 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 96ee861e89f..251ab907832 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 @@ -706,6 +706,9 @@ class VaultItemListingViewModel @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/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 11c2d5360b6..3f62c21c1a7 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 @@ -499,6 +499,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 c18c4edda8d..3a5d2e42232 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 @@ -80,6 +80,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, + 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? = 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 00000000000..fd31a76bcbc --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/model/VaultBankAccountType.kt @@ -0,0 +1,31 @@ +package com.x8bit.bitwarden.ui.vault.model + +/** + * Defines all available account type options for bank accounts. + */ +enum class VaultBankAccountType { + SELECT, + CHECKING, + SAVINGS, + CERTIFICATE_OF_DEPOSIT, + LINE_OF_CREDIT, + INVESTMENT_BROKERAGE, + MONEY_MARKET, + OTHER, +} + +/** + * Returns a [VaultBankAccountType] with the provided [String] or null. + */ +fun String.findVaultBankAccountTypeWithNameOrNull(): VaultBankAccountType? = + VaultBankAccountType + .entries + .find { vaultBankAccountType -> + vaultBankAccountType.name.lowercaseWithoutSpacesOrUnderscores == + this.lowercaseWithoutSpacesOrUnderscores + } + +private val String.lowercaseWithoutSpacesOrUnderscores: String + get() = lowercase() + .replace(" ", "") + .replace("_", "") 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 e5ab22f1daf..18893baa9e3 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/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 0bb6b1bebe2..1a4bd85ad64 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/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 0f9ca15e814..a34d9f027d7 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 @@ -44,6 +44,7 @@ sealed class FlagKey { V2EncryptionKeyConnector, V2EncryptionPassword, V2EncryptionTde, + NewItemTypes, ) } } @@ -170,6 +171,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 f0f6d50bf03..bc8be99a92d 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 @@ -60,6 +60,10 @@ class FlagKeyTest { FlagKey.V2EncryptionTde.keyName, "pm-27279-v2-registration-tde-jit", ) + assertEquals( + FlagKey.NewItemTypes.keyName, + "pm-32009-new-item-types", + ) } @Test @@ -79,6 +83,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 b82cf0d3c08..1a7bed9c316 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 0a03a0cc334..5b403a4180b 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 629485ed96d..4b89be3e86c 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 1ac96dcce2b..3f88bb3054a 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -515,6 +515,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?, @@ -787,6 +796,155 @@ 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 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("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 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 issueYear The issue year (nullable). + * @property expirationMonth The expiration 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("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("issueYear") + val issueYear: String?, + + @SerialName("expirationMonth") + val expirationMonth: String?, + + @SerialName("expirationYear") + val expirationYear: String?, + ) + /** * Represents password history in the vault response. * 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 847e8bd9fd5..2f0155ba743 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? = null, + 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 3bfe9c9ea0b..760c5cc9cfa 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? = null, + 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,97 @@ 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", + 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, + 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", + 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", + issueYear: String? = "mockIssueYear-$number", + expirationMonth: String? = "mockExpirationMonth-$number", + expirationYear: String? = "mockExpirationYear-$number", +): SyncResponseJson.Cipher.Passport = + SyncResponseJson.Cipher.Passport( + surname = surname, + givenName = givenName, + dobMonth = dobMonth, + dobYear = dobYear, + nationality = nationality, + passportNumber = passportNumber, + passportType = passportType, + issuingCountry = issuingCountry, + issuingAuthority = issuingAuthority, + issueMonth = issueMonth, + issueYear = issueYear, + expirationMonth = expirationMonth, + 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 0cbc8f6bca0..deeee5a74c8 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 @@ -38,6 +38,7 @@ fun FlagKey.ListItemContent( FlagKey.V2EncryptionKeyConnector, FlagKey.V2EncryptionPassword, FlagKey.V2EncryptionTde, + FlagKey.NewItemTypes, -> { @Suppress("UNCHECKED_CAST") BooleanFlagItem( @@ -97,4 +98,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 d191d98ad32..e690d650b93 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1266,4 +1266,49 @@ Do you want to switch to this account? Archive item Once archived, this item will be excluded from search results and autofill suggestions. Pricing unavailable + 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 445ac412ce9..f15bc3dd10c 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -51,6 +51,7 @@ V2 Encryption - Key Connector V2 Encryption - JIT Password V2 Encryption - Password + New Item Types From 5f659d47f5ceb9c37c20251003993e91b61daa29 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 7 Apr 2026 19:49:41 -0400 Subject: [PATCH 2/2] Address review findings: feature flag gating, logging, save guard C1: Gate new item types behind pm-32009-new-item-types feature flag in VaultViewModel and VaultItemListingViewModel excluded options. I1: Add Timber.w logging when cipher conversion fails during sync. I2: Add isSdkSupported property to ItemType and guard save path in VaultAddEditViewModel to prevent crash on unsupported types. --- .../util/VaultSdkCipherExtensions.kt | 5 ++++- .../feature/addedit/VaultAddEditViewModel.kt | 19 +++++++++++++++++++ .../itemlisting/VaultItemListingViewModel.kt | 13 +++++++++++-- .../ui/vault/feature/vault/VaultViewModel.kt | 7 ++++++- 4 files changed, 40 insertions(+), 4 deletions(-) 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 d2531f8708e..81ebb01704d 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 @@ -36,6 +36,7 @@ import com.bitwarden.vault.SecureNote import com.bitwarden.vault.SecureNoteType import com.bitwarden.vault.SshKey import com.bitwarden.vault.UriMatchType +import timber.log.Timber /** * Converts a Bitwarden SDK [Cipher] object to a corresponding @@ -387,7 +388,9 @@ private fun CipherType.toNetworkCipherType(): CipherTypeJson = */ fun List.toEncryptedSdkCipherList(): List = mapNotNull { - runCatching { it.toEncryptedSdkCipher() }.getOrNull() + runCatching { it.toEncryptedSdkCipher() } + .onFailure { e -> Timber.w(e, "Failed to convert cipher: %s", it.id) } + .getOrNull() } /** 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 3b9322cd870..619041d2fb3 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 @@ -440,6 +440,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 { @@ -2771,6 +2779,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. * @@ -2966,6 +2979,8 @@ data class VaultAddEditState( override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.BANK_ACCOUNT + override val isSdkSupported: Boolean get() = false + override val vaultLinkedFieldTypes: ImmutableList get() = persistentListOf() } @@ -2988,6 +3003,8 @@ data class VaultAddEditState( override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.DRIVERS_LICENSE + override val isSdkSupported: Boolean get() = false + override val vaultLinkedFieldTypes: ImmutableList get() = persistentListOf() } @@ -3014,6 +3031,8 @@ data class VaultAddEditState( 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/itemlisting/VaultItemListingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 251ab907832..558d98c6b01 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 @@ -114,6 +114,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -152,7 +153,7 @@ class VaultItemListingViewModel @Inject constructor( private val relyingPartyParser: RelyingPartyParser, private val toastManager: ToastManager, snackbarRelayManager: SnackbarRelayManager, - featureFlagManager: FeatureFlagManager, + private val featureFlagManager: FeatureFlagManager, ) : BaseViewModel( initialState = run { val userState = requireNotNull(authRepository.userStateFlow.value) @@ -845,8 +846,15 @@ class VaultItemListingViewModel @Inject constructor( } private fun createVaultItemTypeSelectionExcludedOptions(): ImmutableList { + val isNewItemTypesEnabled = featureFlagManager + .getFeatureFlag(FlagKey.NewItemTypes) + val newItemTypeExclusions = listOfNotNull( + CreateVaultItemType.BANK_ACCOUNT.takeUnless { isNewItemTypesEnabled }, + CreateVaultItemType.DRIVERS_LICENSE.takeUnless { isNewItemTypesEnabled }, + CreateVaultItemType.PASSPORT.takeUnless { isNewItemTypesEnabled }, + ) // If policy is enable for any organization, exclude the card option - return if (state.restrictItemTypesPolicyOrgIds.isNotEmpty()) { + val baseExclusions = if (state.restrictItemTypesPolicyOrgIds.isNotEmpty()) { persistentListOf( CreateVaultItemType.CARD, CreateVaultItemType.FOLDER, @@ -858,6 +866,7 @@ class VaultItemListingViewModel @Inject constructor( CreateVaultItemType.FOLDER, ) } + return (baseExclusions + newItemTypeExclusions).toPersistentList() } private fun handleAddVaultItemClick() { 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 3f62c21c1a7..5d943009aa5 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 { @@ -433,12 +433,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 {