[PM-32009] feat: Add infrastructure for new vault item types (#6828)

This commit is contained in:
Patrick Honkonen
2026-05-04 16:40:44 +00:00
committed by GitHub
parent 67182df91b
commit 8223fb3089
46 changed files with 1595 additions and 62 deletions
@@ -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")
}
/**
@@ -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")
}
/**
@@ -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
@@ -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
}
/**
@@ -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
*/
@@ -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
}
@@ -73,6 +73,9 @@ fun CoachMarkScope<AddEditItemCoachMark>.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<AddEditItemCoachMark>.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(
@@ -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<VaultLinkedFieldType>
/**
* 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<VaultLinkedFieldType>
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<VaultLinkedFieldType>
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<VaultLinkedFieldType>
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<VaultLinkedFieldType>
get() = persistentListOf()
}
}
/**
@@ -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
}
@@ -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,
@@ -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()
}
}
@@ -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,
)
}
}
}
@@ -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()
}
}
@@ -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
@@ -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<SnackbarRelay>,
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
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<CreateVaultItemType> {
// 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
},
),
)
@@ -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<CipherListView>.applyFilters(
@@ -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<SnackbarRelay>,
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
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()
@@ -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(
@@ -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
}
}
@@ -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
}
}
@@ -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,
}
@@ -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
}
@@ -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"
}
"""
@@ -52,7 +52,6 @@ fun createMockCipherView(
clock: Clock = FIXED_CLOCK,
fido2Credentials: List<Fido2Credential>? = 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<AttachmentView> = listOf(createMockAttachmentView(number = number)),
isArchived: Boolean = false,
passwordHistory: List<PasswordHistoryView> = 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].
*/
@@ -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",
)
/**
@@ -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(
@@ -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),
)
}
}
/**
@@ -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)
}
}
@@ -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)
@@ -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")
@@ -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),
)
}
}
/**
@@ -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 {
@@ -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(""))
}
}
@@ -43,6 +43,7 @@ sealed class FlagKey<out T : Any> {
V2EncryptionKeyConnector,
V2EncryptionPassword,
V2EncryptionTde,
NewItemTypes,
)
}
}
@@ -161,6 +162,15 @@ sealed class FlagKey<out T : Any> {
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<Boolean>() {
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.
@@ -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
},
@@ -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?,
@@ -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
@@ -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
@@ -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<String>?,
@@ -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.
*
@@ -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"
}
@@ -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"
}
@@ -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<SyncResponseJson.Cipher.Field>? = 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,
@@ -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<SyncResponseJson.Cipher.Field>? = 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].
*/
@@ -37,6 +37,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.V2EncryptionKeyConnector,
FlagKey.V2EncryptionPassword,
FlagKey.V2EncryptionTde,
FlagKey.NewItemTypes,
-> {
@Suppress("UNCHECKED_CAST")
BooleanFlagItem(
@@ -95,4 +96,5 @@ private fun <T : Any> FlagKey<T>.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)
}
+45
View File
@@ -1295,4 +1295,49 @@ Do you want to switch to this account?</string>
<string name="trouble_loading_portal">We had trouble loading the management portal, so try again.</string>
<string name="subscription_error">Subscription error</string>
<string name="trouble_loading_subscription">We couldnt load your subscription details. Please try again.</string>
<string name="view_bank_account">View bank account</string>
<string name="view_drivers_license">View drivers license</string>
<string name="view_passport">View passport</string>
<string name="new_bank_account">New bank account</string>
<string name="new_drivers_license">New drivers license</string>
<string name="new_passport">New passport</string>
<string name="edit_bank_account">Edit bank account</string>
<string name="edit_drivers_license">Edit drivers license</string>
<string name="edit_passport">Edit passport</string>
<string name="type_bank_account">Bank account</string>
<string name="type_drivers_license">Drivers license</string>
<string name="type_passport">Passport</string>
<string name="bank_accounts">Bank accounts</string>
<string name="drivers_licenses">Drivers licenses</string>
<string name="passports">Passports</string>
<string name="bank_name">Bank name</string>
<string name="name_on_account">Name on account</string>
<string name="account_type">Account type</string>
<string name="account_number">Account number</string>
<string name="routing_number">Routing/transit number</string>
<string name="branch_number">Branch/institution number</string>
<string name="swift_code">SWIFT code</string>
<string name="iban">IBAN</string>
<string name="bank_contact_phone">Bank contact phone</string>
<string name="copy_account_number">Copy account number</string>
<string name="copy_routing_number">Copy routing number</string>
<string name="copy_swift_code">Copy SWIFT code</string>
<string name="copy_iban">Copy IBAN</string>
<string name="checking">Checking</string>
<string name="savings">Savings</string>
<string name="certificate_of_deposit">Certificate of deposit</string>
<string name="line_of_credit">Line of credit</string>
<string name="investment_brokerage">Investment/brokerage</string>
<string name="money_market">Money market</string>
<string name="issuing_country">Issuing country</string>
<string name="issuing_state">Issuing state/province</string>
<string name="license_class">License class</string>
<string name="surname">Surname</string>
<string name="given_name">Given name</string>
<string name="date_of_birth">Date of birth</string>
<string name="nationality">Nationality</string>
<string name="passport_type">Passport type</string>
<string name="issuing_authority">Issuing authority/office</string>
<string name="issue_date">Issue date</string>
<string name="expiration_date">Expiration date</string>
</resources>
@@ -50,6 +50,7 @@
<string name="v2_encryption_key_connector">V2 Encryption - Key Connector</string>
<string name="v2_encryption_jit_password">V2 Encryption - JIT Password</string>
<string name="v2_encryption_password">V2 Encryption - Password</string>
<string name="new_item_types">New Item Types</string>
<!-- endregion Debug Menu -->
</resources>