From ecc4fa6deac7b30e4d1a6762b4b6330ec6dbc777 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Wed, 13 May 2026 10:20:03 -0400 Subject: [PATCH] [PM-32808] feat: Add Add/Edit support for Driver's License item type (#6908) --- .../addedit/VaultAddEditItemContent.kt | 9 +- .../addedit/VaultAddEditLicenseItems.kt | 186 ++++++++++++++++++ .../feature/addedit/VaultAddEditScreen.kt | 6 + .../feature/addedit/VaultAddEditViewModel.kt | 111 ++++++++++- .../VaultAddEditLicenseTypeHandlers.kt | 100 ++++++++++ .../vault/feature/item/VaultItemViewModel.kt | 2 +- .../feature/addedit/VaultAddEditScreenTest.kt | 148 ++++++++++++++ .../addedit/VaultAddEditViewModelTest.kt | 167 +++++++++++++++- .../vault/feature/vault/VaultViewModelTest.kt | 2 +- ui/src/main/res/values/strings.xml | 7 +- 10 files changed, 719 insertions(+), 19 deletions(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLicenseTypeHandlers.kt 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 9fa56d4713..56ef086193 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 @@ -36,6 +36,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditBankAcc import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers +import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLicenseTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers @@ -54,6 +55,7 @@ fun CoachMarkScope.VaultAddEditContent( cardItemTypeHandlers: VaultAddEditCardTypeHandlers, sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers, bankAccountItemTypeHandlers: VaultAddEditBankAccountTypeHandlers, + licenseItemTypeHandlers: VaultAddEditLicenseTypeHandlers, isCardScannerEnabled: Boolean, cardHolderNameFocusRequester: FocusRequester, modifier: Modifier = Modifier, @@ -288,7 +290,12 @@ fun CoachMarkScope.VaultAddEditContent( ) } - is VaultAddEditState.ViewState.Content.ItemType.License -> Unit + is VaultAddEditState.ViewState.Content.ItemType.License -> { + vaultAddEditLicenseItems( + licenseState = state.type, + licenseHandlers = licenseItemTypeHandlers, + ) + } is VaultAddEditState.ViewState.Content.ItemType.Passport -> Unit } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt new file mode 100644 index 0000000000..9cfe03b2c7 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt @@ -0,0 +1,186 @@ +package com.x8bit.bitwarden.ui.vault.feature.addedit + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton +import com.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.bitwarden.ui.platform.components.field.BitwardenTextField +import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.bitwarden.ui.platform.components.model.CardStyle +import com.bitwarden.ui.platform.resource.BitwardenString +import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLicenseTypeHandlers + +/** + * The UI for adding and editing a license cipher. + */ +@Suppress("LongMethod") +fun LazyListScope.vaultAddEditLicenseItems( + licenseState: VaultAddEditState.ViewState.Content.ItemType.License, + licenseHandlers: VaultAddEditLicenseTypeHandlers, +) { + item { + Spacer(modifier = Modifier.height(16.dp)) + BitwardenListHeaderText( + label = stringResource(id = BitwardenString.license_details), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + } + + item { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = BitwardenString.first_name), + value = licenseState.firstName, + onValueChange = licenseHandlers.onFirstNameTextChange, + textFieldTestTag = "LicenseFirstNameEntry", + cardStyle = CardStyle.Top(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextField( + label = stringResource(id = BitwardenString.middle_name), + value = licenseState.middleName, + onValueChange = licenseHandlers.onMiddleNameTextChange, + textFieldTestTag = "LicenseMiddleNameEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextField( + label = stringResource(id = BitwardenString.last_name), + value = licenseState.lastName, + onValueChange = licenseHandlers.onLastNameTextChange, + textFieldTestTag = "LicenseLastNameEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenPasswordField( + label = stringResource(id = BitwardenString.license_number), + value = licenseState.licenseNumber, + onValueChange = licenseHandlers.onLicenseNumberTextChange, + passwordFieldTestTag = "LicenseLicenseNumberEntry", + showPasswordTestTag = "LicenseShowLicenseNumberButton", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextSelectionButton( + label = stringResource(id = BitwardenString.date_of_birth), + selectedOption = licenseState.dateOfBirth, + // TODO: Open a native Material date picker (separate ticket TBD). + onClick = {}, + textFieldTestTag = "LicenseDateOfBirthEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextField( + label = stringResource(id = BitwardenString.issuing_country), + value = licenseState.issuingCountry, + onValueChange = licenseHandlers.onIssuingCountryTextChange, + textFieldTestTag = "LicenseIssuingCountryEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextField( + label = stringResource(id = BitwardenString.issuing_state), + value = licenseState.issuingState, + onValueChange = licenseHandlers.onIssuingStateTextChange, + textFieldTestTag = "LicenseIssuingStateEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextField( + label = stringResource(id = BitwardenString.issuing_authority), + value = licenseState.issuingAuthority, + onValueChange = licenseHandlers.onIssuingAuthorityTextChange, + textFieldTestTag = "LicenseIssuingAuthorityEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextSelectionButton( + label = stringResource(id = BitwardenString.issue_date), + selectedOption = licenseState.issueDate, + // TODO: Open a native Material date picker (separate ticket TBD). + onClick = {}, + textFieldTestTag = "LicenseIssueDateEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextSelectionButton( + label = stringResource(id = BitwardenString.expiration_date), + selectedOption = licenseState.expirationDate, + // TODO: Open a native Material date picker (separate ticket TBD). + onClick = {}, + textFieldTestTag = "LicenseExpirationDateEntry", + cardStyle = CardStyle.Middle(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextField( + label = stringResource(id = BitwardenString.license_class), + value = licenseState.licenseClass, + onValueChange = licenseHandlers.onLicenseClassTextChange, + textFieldTestTag = "LicenseLicenseClassEntry", + cardStyle = CardStyle.Bottom, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt index 77198a6d34..d7150af964 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt @@ -86,6 +86,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditBankAcc import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers +import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLicenseTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditUserVerificationHandlers @@ -241,6 +242,10 @@ fun VaultAddEditScreen( VaultAddEditBankAccountTypeHandlers.create(viewModel = viewModel) } + val licenseItemTypeHandlers = remember(viewModel) { + VaultAddEditLicenseTypeHandlers.create(viewModel = viewModel) + } + val archiveClickAction = { viewModel.trySendAction(VaultAddEditAction.Common.ArchiveClick) } val unarchiveClickAction = { viewModel.trySendAction(VaultAddEditAction.Common.UnarchiveClick) } @@ -423,6 +428,7 @@ fun VaultAddEditScreen( cardItemTypeHandlers = cardItemTypeHandlers, sshKeyItemTypeHandlers = sshKeyItemTypeHandlers, bankAccountItemTypeHandlers = bankAccountItemTypeHandlers, + licenseItemTypeHandlers = licenseItemTypeHandlers, isCardScannerEnabled = state.isCardScannerEnabled, cardHolderNameFocusRequester = cardHolderNameFocusRequester, lazyListState = lazyListState, 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 729021b87d..71efdb6dea 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 @@ -314,6 +314,10 @@ class VaultAddEditViewModel @Inject constructor( handleBankAccountTypeActions(action) } + is VaultAddEditAction.ItemType.LicenseType -> { + handleLicenseTypeActions(action) + } + is VaultAddEditAction.Internal -> handleInternalActions(action) } } @@ -1746,6 +1750,49 @@ class VaultAddEditViewModel @Inject constructor( //endregion Bank Account Type Handlers + //region License Type Handlers + + @Suppress("LongMethod") + private fun handleLicenseTypeActions( + action: VaultAddEditAction.ItemType.LicenseType, + ) { + when (action) { + is VaultAddEditAction.ItemType.LicenseType.FirstNameTextChange -> { + updateLicenseContent { it.copy(firstName = action.firstName) } + } + + is VaultAddEditAction.ItemType.LicenseType.MiddleNameTextChange -> { + updateLicenseContent { it.copy(middleName = action.middleName) } + } + + is VaultAddEditAction.ItemType.LicenseType.LastNameTextChange -> { + updateLicenseContent { it.copy(lastName = action.lastName) } + } + + is VaultAddEditAction.ItemType.LicenseType.LicenseNumberTextChange -> { + updateLicenseContent { it.copy(licenseNumber = action.licenseNumber) } + } + + is VaultAddEditAction.ItemType.LicenseType.IssuingCountryTextChange -> { + updateLicenseContent { it.copy(issuingCountry = action.country) } + } + + is VaultAddEditAction.ItemType.LicenseType.IssuingStateTextChange -> { + updateLicenseContent { it.copy(issuingState = action.state) } + } + + is VaultAddEditAction.ItemType.LicenseType.IssuingAuthorityTextChange -> { + updateLicenseContent { it.copy(issuingAuthority = action.authority) } + } + + is VaultAddEditAction.ItemType.LicenseType.LicenseClassTextChange -> { + updateLicenseContent { it.copy(licenseClass = action.licenseClass) } + } + } + } + + //endregion License Type Handlers + //region Internal Type Handlers private fun handleInternalActions(action: VaultAddEditAction.Internal) { @@ -2513,6 +2560,16 @@ class VaultAddEditViewModel @Inject constructor( } } + private inline fun updateLicenseContent( + crossinline block: (VaultAddEditState.ViewState.Content.ItemType.License) -> + VaultAddEditState.ViewState.Content.ItemType.License, + ) { + updateContent { currentContent -> + (currentContent.type as? VaultAddEditState.ViewState.Content.ItemType.License) + ?.let { currentContent.copy(type = block(it)) } + } + } + @Suppress("MaxLineLength") private suspend fun VaultAddEditState.ViewState.Content.createCipherForAddAndCloneItemStates(): CreateCipherResult { return common.selectedOwner?.collections @@ -2619,7 +2676,7 @@ data class VaultAddEditState( 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.DRIVERS_LICENSE -> BitwardenString.new_license.asText() VaultItemCipherType.PASSPORT -> BitwardenString.new_passport.asText() } @@ -2630,7 +2687,7 @@ data class VaultAddEditState( 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.DRIVERS_LICENSE -> BitwardenString.edit_license.asText() VaultItemCipherType.PASSPORT -> BitwardenString.edit_passport.asText() } } @@ -2724,7 +2781,7 @@ data class VaultAddEditState( SECURE_NOTES(BitwardenString.type_secure_note), SSH_KEYS(BitwardenString.type_ssh_key), BANK_ACCOUNT(BitwardenString.type_bank_account), - DRIVERS_LICENSE(BitwardenString.type_license), + LICENSE(BitwardenString.type_license), PASSPORT(BitwardenString.type_passport), } @@ -3067,7 +3124,7 @@ data class VaultAddEditState( val licenseClass: String = "", ) : ItemType() { override val itemTypeOption: ItemTypeOption - get() = ItemTypeOption.DRIVERS_LICENSE + get() = ItemTypeOption.LICENSE override val isSdkSupported: Boolean get() = false @@ -4082,6 +4139,52 @@ sealed class VaultAddEditAction { */ data class BankContactPhoneTextChange(val phone: String) : BankAccountType() } + + /** + * Represents actions specific to the License type. + */ + sealed class LicenseType : ItemType() { + + /** + * Fired when the first name text input is changed. + */ + data class FirstNameTextChange(val firstName: String) : LicenseType() + + /** + * Fired when the middle name text input is changed. + */ + data class MiddleNameTextChange(val middleName: String) : LicenseType() + + /** + * Fired when the last name text input is changed. + */ + data class LastNameTextChange(val lastName: String) : LicenseType() + + /** + * Fired when the license number text input is changed. + */ + data class LicenseNumberTextChange(val licenseNumber: String) : LicenseType() + + /** + * Fired when the issuing country text input is changed. + */ + data class IssuingCountryTextChange(val country: String) : LicenseType() + + /** + * Fired when the issuing state/province text input is changed. + */ + data class IssuingStateTextChange(val state: String) : LicenseType() + + /** + * Fired when the issuing authority text input is changed. + */ + data class IssuingAuthorityTextChange(val authority: String) : LicenseType() + + /** + * Fired when the license class text input is changed. + */ + data class LicenseClassTextChange(val licenseClass: String) : LicenseType() + } } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLicenseTypeHandlers.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLicenseTypeHandlers.kt new file mode 100644 index 0000000000..cc71010607 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLicenseTypeHandlers.kt @@ -0,0 +1,100 @@ +package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers + +import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction +import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel + +/** + * A collection of handler functions for managing user interactions on the License portion of the + * Add/Edit cipher screen. + * + * @property onFirstNameTextChange Handles changes to the first name text input. + * @property onMiddleNameTextChange Handles changes to the middle name text input. + * @property onLastNameTextChange Handles changes to the last name text input. + * @property onLicenseNumberTextChange Handles changes to the license number text input. + * @property onIssuingCountryTextChange Handles changes to the issuing country text input. + * @property onIssuingStateTextChange Handles changes to the issuing state/province text input. + * @property onIssuingAuthorityTextChange Handles changes to the issuing authority text input. + * @property onLicenseClassTextChange Handles changes to the license class text input. + */ +@Suppress("LongParameterList") +data class VaultAddEditLicenseTypeHandlers( + val onFirstNameTextChange: (String) -> Unit, + val onMiddleNameTextChange: (String) -> Unit, + val onLastNameTextChange: (String) -> Unit, + val onLicenseNumberTextChange: (String) -> Unit, + val onIssuingCountryTextChange: (String) -> Unit, + val onIssuingStateTextChange: (String) -> Unit, + val onIssuingAuthorityTextChange: (String) -> Unit, + val onLicenseClassTextChange: (String) -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + + /** + * Creates an instance of [VaultAddEditLicenseTypeHandlers] by binding actions to + * the provided [VaultAddEditViewModel]. + */ + @Suppress("LongMethod") + fun create( + viewModel: VaultAddEditViewModel, + ): VaultAddEditLicenseTypeHandlers = + VaultAddEditLicenseTypeHandlers( + onFirstNameTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.FirstNameTextChange( + firstName = it, + ), + ) + }, + onMiddleNameTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.MiddleNameTextChange( + middleName = it, + ), + ) + }, + onLastNameTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LastNameTextChange( + lastName = it, + ), + ) + }, + onLicenseNumberTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LicenseNumberTextChange( + licenseNumber = it, + ), + ) + }, + onIssuingCountryTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingCountryTextChange( + country = it, + ), + ) + }, + onIssuingStateTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingStateTextChange( + state = it, + ), + ) + }, + onIssuingAuthorityTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingAuthorityTextChange( + authority = it, + ), + ) + }, + onLicenseClassTextChange = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LicenseClassTextChange( + licenseClass = it, + ), + ) + }, + ) + } +} 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 abb9b423bd..47500bf81d 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 @@ -1692,7 +1692,7 @@ data class VaultItemState( 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.DRIVERS_LICENSE -> BitwardenString.view_license.asText() VaultItemCipherType.PASSPORT -> BitwardenString.view_passport.asText() } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index 950de0bd2e..5b01ed4f96 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -2779,6 +2779,138 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { } } + @Test + fun `in ItemType_License changing first name should trigger FirstNameTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "First name") + .performTextInput(text = "Missy") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.FirstNameTextChange( + firstName = "Missy", + ), + ) + } + } + + @Test + fun `in ItemType_License changing middle name should trigger MiddleNameTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "Middle name") + .performTextInput(text = "Anne") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.MiddleNameTextChange( + middleName = "Anne", + ), + ) + } + } + + @Test + fun `in ItemType_License changing last name should trigger LastNameTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "Last name") + .performTextInput(text = "Katner") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LastNameTextChange( + lastName = "Katner", + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_License changing the license number text field should trigger LicenseNumberTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "License number") + .performTextInput(text = "K123-456-789") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LicenseNumberTextChange( + licenseNumber = "K123-456-789", + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_License changing the issuing country text field should trigger IssuingCountryTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "Issuing country") + .performTextInput(text = "USA") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingCountryTextChange( + country = "USA", + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_License changing the issuing state text field should trigger IssuingStateTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "Issuing state / province") + .performTextInput(text = "Wisconsin") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingStateTextChange( + state = "Wisconsin", + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_License changing the issuing authority text field should trigger IssuingAuthorityTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "Issuing authority") + .performTextInput(text = "DMV") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingAuthorityTextChange( + authority = "DMV", + ), + ) + } + } + + @Test + fun `in ItemType_License changing license class should trigger LicenseClassTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_LICENSE + composeTestRule + .onNodeWithTextAfterScroll(text = "License class") + .performTextInput(text = "Class D") + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LicenseClassTextChange( + licenseClass = "Class D", + ), + ) + } + } + @Test fun `clicking Add field button should allow creation of Linked type`() { mutableStateFlow.value = DEFAULT_STATE_LOGIN @@ -4889,6 +5021,22 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { isCardScannerEnabled = false, ) + private val DEFAULT_STATE_LICENSE = VaultAddEditState( + vaultAddEditType = VaultAddEditType.AddItem, + cipherType = VaultItemCipherType.DRIVERS_LICENSE, + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + type = VaultAddEditState.ViewState.Content.ItemType.License(), + isIndividualVaultDisabled = false, + ), + dialog = null, + bottomSheetState = null, + shouldShowCoachMarkTour = false, + defaultUriMatchType = UriMatchTypeModel.EXACT, + hasPremium = false, + isCardScannerEnabled = false, + ) + private val DEFAULT_STATE_SECURE_NOTES_CUSTOM_FIELDS = VaultAddEditState( viewState = VaultAddEditState.ViewState.Content( common = VaultAddEditState.ViewState.Content.Common( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index c4617a406b..d8bd7a1bfb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -2408,9 +2408,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } - @Suppress("MaxLineLength") @Test - fun `in add mode, SaveClick with a Drivers License item should emit ShowSnackbar without saving`() = + fun `in add mode, SaveClick with a License item should emit ShowSnackbar without saving`() = runTest { mutableVaultDataFlow.value = DataState.Loaded(createVaultData()) val licenseState = createVaultAddItemState( @@ -2522,12 +2521,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { assertTrue(itemType.vaultLinkedFieldTypes.isEmpty()) } - @Suppress("MaxLineLength") @Test - fun `ItemType DriversLicense should expose DRIVERS_LICENSE itemTypeOption and not be SDK supported`() { + fun `ItemType License should expose DRIVERS_LICENSE itemTypeOption and not be SDK supported`() { val itemType = VaultAddEditState.ViewState.Content.ItemType.License() assertEquals( - VaultAddEditState.ItemTypeOption.DRIVERS_LICENSE, + VaultAddEditState.ItemTypeOption.LICENSE, itemType.itemTypeOption, ) assertFalse(itemType.isSdkSupported) @@ -2554,7 +2552,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { baseAddState.copy(cipherType = VaultItemCipherType.BANK_ACCOUNT).screenDisplayName, ) assertEquals( - BitwardenString.new_drivers_license.asText(), + BitwardenString.new_license.asText(), baseAddState.copy(cipherType = VaultItemCipherType.DRIVERS_LICENSE).screenDisplayName, ) assertEquals( @@ -2574,7 +2572,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { baseEditState.copy(cipherType = VaultItemCipherType.BANK_ACCOUNT).screenDisplayName, ) assertEquals( - BitwardenString.edit_drivers_license.asText(), + BitwardenString.edit_license.asText(), baseEditState.copy(cipherType = VaultItemCipherType.DRIVERS_LICENSE).screenDisplayName, ) assertEquals( @@ -2594,7 +2592,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { baseCloneState.copy(cipherType = VaultItemCipherType.BANK_ACCOUNT).screenDisplayName, ) assertEquals( - BitwardenString.new_drivers_license.asText(), + BitwardenString.new_license.asText(), baseCloneState.copy(cipherType = VaultItemCipherType.DRIVERS_LICENSE).screenDisplayName, ) assertEquals( @@ -4189,6 +4187,159 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Nested + inner class VaultAddEditLicenseTypeItemActions { + private lateinit var viewModel: VaultAddEditViewModel + private lateinit var vaultAddItemInitialState: VaultAddEditState + private lateinit var licenseInitialSavedStateHandle: SavedStateHandle + + @BeforeEach + fun setup() { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherListView = createMockCipherListView(1)), + ) + vaultAddItemInitialState = createVaultAddItemState( + vaultItemCipherType = VaultItemCipherType.DRIVERS_LICENSE, + typeContentViewState = + VaultAddEditState.ViewState.Content.ItemType.License(), + ) + licenseInitialSavedStateHandle = createSavedStateHandleWithState( + state = vaultAddItemInitialState, + vaultAddEditType = VaultAddEditType.AddItem, + vaultItemCipherType = VaultItemCipherType.DRIVERS_LICENSE, + ) + viewModel = createAddVaultItemViewModel( + savedStateHandle = licenseInitialSavedStateHandle, + ) + } + + private fun expectedLicense( + block: VaultAddEditState.ViewState.Content.ItemType.License.() -> + VaultAddEditState.ViewState.Content.ItemType.License, + ): VaultAddEditState = + createVaultAddItemState( + vaultItemCipherType = VaultItemCipherType.DRIVERS_LICENSE, + typeContentViewState = VaultAddEditState + .ViewState + .Content + .ItemType + .License() + .block(), + ) + + @Test + fun `FirstNameTextChange should update first name`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.FirstNameTextChange( + firstName = "Missy", + ), + ) + + assertEquals( + expectedLicense { copy(firstName = "Missy") }, + viewModel.stateFlow.value, + ) + } + + @Test + fun `MiddleNameTextChange should update middle name`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.MiddleNameTextChange( + middleName = "Anne", + ), + ) + + assertEquals( + expectedLicense { copy(middleName = "Anne") }, + viewModel.stateFlow.value, + ) + } + + @Test + fun `LastNameTextChange should update last name`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LastNameTextChange( + lastName = "Katner", + ), + ) + + assertEquals( + expectedLicense { copy(lastName = "Katner") }, + viewModel.stateFlow.value, + ) + } + + @Test + fun `LicenseNumberTextChange should update license number`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LicenseNumberTextChange( + licenseNumber = "K123-456-789", + ), + ) + + assertEquals( + expectedLicense { copy(licenseNumber = "K123-456-789") }, + viewModel.stateFlow.value, + ) + } + + @Test + fun `IssuingCountryTextChange should update issuing country`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingCountryTextChange( + country = "USA", + ), + ) + + assertEquals( + expectedLicense { copy(issuingCountry = "USA") }, + viewModel.stateFlow.value, + ) + } + + @Test + fun `IssuingStateTextChange should update issuing state`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingStateTextChange( + state = "Wisconsin", + ), + ) + + assertEquals( + expectedLicense { copy(issuingState = "Wisconsin") }, + viewModel.stateFlow.value, + ) + } + + @Test + fun `IssuingAuthorityTextChange should update issuing authority`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.IssuingAuthorityTextChange( + authority = "DMV", + ), + ) + + assertEquals( + expectedLicense { copy(issuingAuthority = "DMV") }, + viewModel.stateFlow.value, + ) + } + + @Test + fun `LicenseClassTextChange should update license class`() = runTest { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LicenseType.LicenseClassTextChange( + licenseClass = "Class D", + ), + ) + + assertEquals( + expectedLicense { copy(licenseClass = "Class D") }, + viewModel.stateFlow.value, + ) + } + } + @Test fun `NumberVisibilityChange should log an event when in edit mode and password is visible`() = runTest { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 35572ca44f..91c0038541 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -3831,7 +3831,7 @@ class VaultViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `SelectAddItemType action should not exclude bank account, drivers license, or passport when NewItemTypes flag is enabled`() { + fun `SelectAddItemType action should not exclude bank account, license, or passport when NewItemTypes flag is enabled`() { mutableNewItemTypesFlagFlow.value = true val viewModel = createViewModel() diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 49995da4ee..8bd1f5b189 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1306,19 +1306,18 @@ Do you want to switch to this account? Subscription error We couldn’t load your subscription details. Please try again. View bank account - View license + View license View passport New bank account - New license + New license New passport Edit bank account - Edit license + Edit license Edit passport Bank account License Passport Bank accounts - Driver’s licenses Passports Bank name Name on account