[PM-32806] feat: Add Add/Edit support for Passport item type (#6923)

This commit is contained in:
Patrick Honkonen
2026-05-15 12:14:13 -04:00
committed by GitHub
parent cc06636276
commit 484d326e14
9 changed files with 951 additions and 22 deletions

View File

@@ -38,6 +38,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonH
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.VaultAddEditPassportTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
/**
@@ -56,6 +57,7 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers,
bankAccountItemTypeHandlers: VaultAddEditBankAccountTypeHandlers,
licenseItemTypeHandlers: VaultAddEditLicenseTypeHandlers,
passportItemTypeHandlers: VaultAddEditPassportTypeHandlers,
isCardScannerEnabled: Boolean,
cardHolderNameFocusRequester: FocusRequester,
modifier: Modifier = Modifier,
@@ -296,7 +298,13 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
licenseHandlers = licenseItemTypeHandlers,
)
}
is VaultAddEditState.ViewState.Content.ItemType.Passport -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Passport -> {
vaultAddEditPassportItems(
passportState = state.type,
passportHandlers = passportItemTypeHandlers,
)
}
}
vaultAddEditAdditionalOptions(

View File

@@ -0,0 +1,213 @@
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.VaultAddEditPassportTypeHandlers
/**
* The UI for adding and editing a passport cipher.
*/
@Suppress("LongMethod")
fun LazyListScope.vaultAddEditPassportItems(
passportState: VaultAddEditState.ViewState.Content.ItemType.Passport,
passportHandlers: VaultAddEditPassportTypeHandlers,
) {
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.passport_details),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = BitwardenString.first_name),
value = passportState.givenName,
onValueChange = passportHandlers.onGivenNameTextChange,
textFieldTestTag = "PassportGivenNameEntry",
cardStyle = CardStyle.Top(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = BitwardenString.last_name),
value = passportState.surname,
onValueChange = passportHandlers.onSurnameTextChange,
textFieldTestTag = "PassportSurnameEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextSelectionButton(
label = stringResource(id = BitwardenString.date_of_birth),
selectedOption = passportState.dateOfBirth,
// TODO: Open a native Material date picker (separate ticket TBD).
onClick = {},
textFieldTestTag = "PassportDateOfBirthEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = BitwardenString.sex),
value = passportState.sex,
onValueChange = passportHandlers.onSexTextChange,
textFieldTestTag = "PassportSexEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = BitwardenString.birth_place),
value = passportState.birthPlace,
onValueChange = passportHandlers.onBirthPlaceTextChange,
textFieldTestTag = "PassportBirthPlaceEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = BitwardenString.nationality),
value = passportState.nationality,
onValueChange = passportHandlers.onNationalityTextChange,
textFieldTestTag = "PassportNationalityEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenPasswordField(
label = stringResource(id = BitwardenString.passport_number),
value = passportState.passportNumber,
onValueChange = passportHandlers.onPassportNumberTextChange,
passwordFieldTestTag = "PassportPassportNumberEntry",
showPasswordTestTag = "PassportShowPassportNumberButton",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = BitwardenString.passport_type),
value = passportState.passportType,
onValueChange = passportHandlers.onPassportTypeTextChange,
textFieldTestTag = "PassportPassportTypeEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenPasswordField(
label = stringResource(id = BitwardenString.national_identification_number),
value = passportState.nationalIdentificationNumber,
onValueChange = passportHandlers.onNationalIdentificationNumberTextChange,
passwordFieldTestTag = "PassportNationalIdentificationNumberEntry",
showPasswordTestTag = "PassportShowNationalIdentificationNumberButton",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = BitwardenString.issuing_country),
value = passportState.issuingCountry,
onValueChange = passportHandlers.onIssuingCountryTextChange,
textFieldTestTag = "PassportIssuingCountryEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = BitwardenString.issuing_authority),
value = passportState.issuingAuthority,
onValueChange = passportHandlers.onIssuingAuthorityTextChange,
textFieldTestTag = "PassportIssuingAuthorityEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextSelectionButton(
label = stringResource(id = BitwardenString.issue_date),
selectedOption = passportState.issueDate,
// TODO: Open a native Material date picker (separate ticket TBD).
onClick = {},
textFieldTestTag = "PassportIssueDateEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextSelectionButton(
label = stringResource(id = BitwardenString.expiration_date),
selectedOption = passportState.expirationDate,
// TODO: Open a native Material date picker (separate ticket TBD).
onClick = {},
textFieldTestTag = "PassportExpirationDateEntry",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}

View File

@@ -87,6 +87,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTyp
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.rememberVaultAddEditPassportTypeHandlers
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
@@ -246,6 +247,8 @@ fun VaultAddEditScreen(
VaultAddEditLicenseTypeHandlers.create(viewModel = viewModel)
}
val passportItemTypeHandlers = rememberVaultAddEditPassportTypeHandlers(viewModel = viewModel)
val archiveClickAction = { viewModel.trySendAction(VaultAddEditAction.Common.ArchiveClick) }
val unarchiveClickAction = { viewModel.trySendAction(VaultAddEditAction.Common.UnarchiveClick) }
@@ -429,6 +432,7 @@ fun VaultAddEditScreen(
sshKeyItemTypeHandlers = sshKeyItemTypeHandlers,
bankAccountItemTypeHandlers = bankAccountItemTypeHandlers,
licenseItemTypeHandlers = licenseItemTypeHandlers,
passportItemTypeHandlers = passportItemTypeHandlers,
isCardScannerEnabled = state.isCardScannerEnabled,
cardHolderNameFocusRequester = cardHolderNameFocusRequester,
lazyListState = lazyListState,

View File

@@ -318,6 +318,10 @@ class VaultAddEditViewModel @Inject constructor(
handleLicenseTypeActions(action)
}
is VaultAddEditAction.ItemType.PassportType -> {
handlePassportTypeActions(action)
}
is VaultAddEditAction.Internal -> handleInternalActions(action)
}
}
@@ -1793,6 +1797,59 @@ class VaultAddEditViewModel @Inject constructor(
//endregion License Type Handlers
//region Passport Type Handlers
@Suppress("LongMethod")
private fun handlePassportTypeActions(
action: VaultAddEditAction.ItemType.PassportType,
) {
when (action) {
is VaultAddEditAction.ItemType.PassportType.GivenNameTextChange -> {
updatePassportContent { it.copy(givenName = action.givenName) }
}
is VaultAddEditAction.ItemType.PassportType.SurnameTextChange -> {
updatePassportContent { it.copy(surname = action.surname) }
}
is VaultAddEditAction.ItemType.PassportType.SexTextChange -> {
updatePassportContent { it.copy(sex = action.sex) }
}
is VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange -> {
updatePassportContent { it.copy(birthPlace = action.birthPlace) }
}
is VaultAddEditAction.ItemType.PassportType.NationalityTextChange -> {
updatePassportContent { it.copy(nationality = action.nationality) }
}
is VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange -> {
updatePassportContent { it.copy(passportNumber = action.passportNumber) }
}
is VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange -> {
updatePassportContent { it.copy(passportType = action.passportType) }
}
is VaultAddEditAction.ItemType.PassportType.NationalIdentificationNumberTextChange -> {
updatePassportContent {
it.copy(nationalIdentificationNumber = action.nationalIdentificationNumber)
}
}
is VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange -> {
updatePassportContent { it.copy(issuingCountry = action.country) }
}
is VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange -> {
updatePassportContent { it.copy(issuingAuthority = action.authority) }
}
}
}
//endregion Passport Type Handlers
//region Internal Type Handlers
private fun handleInternalActions(action: VaultAddEditAction.Internal) {
@@ -2570,6 +2627,16 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private inline fun updatePassportContent(
crossinline block: (VaultAddEditState.ViewState.Content.ItemType.Passport) ->
VaultAddEditState.ViewState.Content.ItemType.Passport,
) {
updateContent { currentContent ->
(currentContent.type as? VaultAddEditState.ViewState.Content.ItemType.Passport)
?.let { currentContent.copy(type = block(it)) }
}
}
@Suppress("MaxLineLength")
private suspend fun VaultAddEditState.ViewState.Content.createCipherForAddAndCloneItemStates(): CreateCipherResult {
return common.selectedOwner?.collections
@@ -3154,8 +3221,6 @@ data class VaultAddEditState(
override val itemTypeOption: ItemTypeOption
get() = ItemTypeOption.PASSPORT
override val isSdkSupported: Boolean get() = false
override val vaultLinkedFieldTypes: ImmutableList<VaultLinkedFieldType>
get() = persistentListOf()
}
@@ -4140,6 +4205,64 @@ sealed class VaultAddEditAction {
data class BankContactPhoneTextChange(val phone: String) : BankAccountType()
}
/**
* Represents actions specific to the Passport type.
*/
sealed class PassportType : ItemType() {
/**
* Fired when the given name text input is changed.
*/
data class GivenNameTextChange(val givenName: String) : PassportType()
/**
* Fired when the surname text input is changed.
*/
data class SurnameTextChange(val surname: String) : PassportType()
/**
* Fired when the sex text input is changed.
*/
data class SexTextChange(val sex: String) : PassportType()
/**
* Fired when the birth place text input is changed.
*/
data class BirthPlaceTextChange(val birthPlace: String) : PassportType()
/**
* Fired when the nationality text input is changed.
*/
data class NationalityTextChange(val nationality: String) : PassportType()
/**
* Fired when the passport number text input is changed.
*/
data class PassportNumberTextChange(val passportNumber: String) : PassportType()
/**
* Fired when the passport type text input is changed.
*/
data class PassportTypeTextChange(val passportType: String) : PassportType()
/**
* Fired when the national identification number text input is changed.
*/
data class NationalIdentificationNumberTextChange(
val nationalIdentificationNumber: String,
) : PassportType()
/**
* Fired when the issuing country text input is changed.
*/
data class IssuingCountryTextChange(val country: String) : PassportType()
/**
* Fired when the issuing authority text input is changed.
*/
data class IssuingAuthorityTextChange(val authority: String) : PassportType()
}
/**
* Represents actions specific to the License type.
*/

View File

@@ -0,0 +1,134 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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 Passport portion of the
* Add/Edit cipher screen.
*
* @property onGivenNameTextChange Handles changes to the given name (first name) text input.
* @property onSurnameTextChange Handles changes to the surname (last name) text input.
* @property onSexTextChange Handles changes to the sex text input.
* @property onBirthPlaceTextChange Handles changes to the birth place text input.
* @property onNationalityTextChange Handles changes to the nationality text input.
* @property onPassportNumberTextChange Handles changes to the passport number text input.
* @property onPassportTypeTextChange Handles changes to the passport type text input.
* @property onNationalIdentificationNumberTextChange Handles changes to the national identification
* number text input.
* @property onIssuingCountryTextChange Handles changes to the issuing country text input.
* @property onIssuingAuthorityTextChange Handles changes to the issuing authority text input.
*/
@Suppress("LongParameterList")
data class VaultAddEditPassportTypeHandlers(
val onGivenNameTextChange: (String) -> Unit,
val onSurnameTextChange: (String) -> Unit,
val onSexTextChange: (String) -> Unit,
val onBirthPlaceTextChange: (String) -> Unit,
val onNationalityTextChange: (String) -> Unit,
val onPassportNumberTextChange: (String) -> Unit,
val onPassportTypeTextChange: (String) -> Unit,
val onNationalIdentificationNumberTextChange: (String) -> Unit,
val onIssuingCountryTextChange: (String) -> Unit,
val onIssuingAuthorityTextChange: (String) -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Creates an instance of [VaultAddEditPassportTypeHandlers] by binding actions to
* the provided [VaultAddEditViewModel].
*/
@Suppress("LongMethod")
fun create(
viewModel: VaultAddEditViewModel,
): VaultAddEditPassportTypeHandlers =
VaultAddEditPassportTypeHandlers(
onGivenNameTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.GivenNameTextChange(
givenName = it,
),
)
},
onSurnameTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.SurnameTextChange(
surname = it,
),
)
},
onSexTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.SexTextChange(sex = it),
)
},
onBirthPlaceTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange(
birthPlace = it,
),
)
},
onNationalityTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.NationalityTextChange(
nationality = it,
),
)
},
onPassportNumberTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange(
passportNumber = it,
),
)
},
onPassportTypeTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange(
passportType = it,
),
)
},
onNationalIdentificationNumberTextChange = {
viewModel.trySendAction(
VaultAddEditAction
.ItemType
.PassportType
.NationalIdentificationNumberTextChange(
nationalIdentificationNumber = it,
),
)
},
onIssuingCountryTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange(
country = it,
),
)
},
onIssuingAuthorityTextChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange(
authority = it,
),
)
},
)
}
}
/**
* Helper function to remember a [VaultAddEditPassportTypeHandlers] instance in a [Composable]
* scope.
*/
@Composable
fun rememberVaultAddEditPassportTypeHandlers(
viewModel: VaultAddEditViewModel,
): VaultAddEditPassportTypeHandlers =
remember(viewModel) {
VaultAddEditPassportTypeHandlers.create(viewModel = viewModel)
}

View File

@@ -88,9 +88,10 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCipherType(): CipherT
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.License,
is VaultAddEditState.ViewState.Content.ItemType.Passport,
-> throw IllegalArgumentException("SDK mapping not yet available for $this")
is VaultAddEditState.ViewState.Content.ItemType.Passport -> CipherType.PASSPORT
is VaultAddEditState.ViewState.Content.ItemType.License -> {
throw IllegalArgumentException("SDK mapping not yet available for $this")
}
}
private fun VaultAddEditState.ViewState.Content.ItemType.toSshKeyView(): SshKeyView? =

View File

@@ -2911,6 +2911,171 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `in ItemType_Passport changing first name should trigger GivenNameTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "First name")
.performTextInput(text = "Bruce")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.GivenNameTextChange(
givenName = "Bruce",
),
)
}
}
@Test
fun `in ItemType_Passport changing last name should trigger SurnameTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Last name")
.performTextInput(text = "Wayne")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.SurnameTextChange(
surname = "Wayne",
),
)
}
}
@Test
fun `in ItemType_Passport changing sex should trigger SexTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Sex")
.performTextInput(text = "M")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.SexTextChange(sex = "M"),
)
}
}
@Test
fun `in ItemType_Passport changing birth place should trigger BirthPlaceTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Birth place")
.performTextInput(text = "Gotham City")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange(
birthPlace = "Gotham City",
),
)
}
}
@Test
fun `in ItemType_Passport changing nationality should trigger NationalityTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Nationality")
.performTextInput(text = "American")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.NationalityTextChange(
nationality = "American",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Passport changing the passport number text field should trigger PassportNumberTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Passport number")
.performTextInput(text = "X12345678")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange(
passportNumber = "X12345678",
),
)
}
}
@Test
fun `in ItemType_Passport changing passport type should trigger PassportTypeTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Passport type")
.performTextInput(text = "Regular")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange(
passportType = "Regular",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Passport changing the national identification number text field should trigger NationalIdentificationNumberTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "National identification number")
.performTextInput(text = "987-65-4321")
verify {
viewModel.trySendAction(
VaultAddEditAction
.ItemType
.PassportType
.NationalIdentificationNumberTextChange(
nationalIdentificationNumber = "987-65-4321",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Passport changing the issuing country text field should trigger IssuingCountryTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Issuing country")
.performTextInput(text = "USA")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange(
country = "USA",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Passport changing the issuing authority text field should trigger IssuingAuthorityTextChange`() {
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
composeTestRule
.onNodeWithTextAfterScroll(text = "Issuing authority")
.performTextInput(text = "U.S. Department of State")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange(
authority = "U.S. Department of State",
),
)
}
}
@Test
fun `clicking Add field button should allow creation of Linked type`() {
mutableStateFlow.value = DEFAULT_STATE_LOGIN
@@ -5037,6 +5202,22 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
isCardScannerEnabled = false,
)
private val DEFAULT_STATE_PASSPORT = VaultAddEditState(
vaultAddEditType = VaultAddEditType.AddItem,
cipherType = VaultItemCipherType.PASSPORT,
viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
type = VaultAddEditState.ViewState.Content.ItemType.Passport(),
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(

View File

@@ -2444,14 +2444,21 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `in add mode, SaveClick with a Passport item should emit ShowSnackbar without saving`() =
fun `in add mode, SaveClick with a Passport item should not short-circuit and should run validation`() =
runTest {
mutableVaultDataFlow.value = DataState.Loaded(createVaultData())
val passportState = createVaultAddItemState(
vaultItemCipherType = VaultItemCipherType.PASSPORT,
commonContentViewState = createCommonContentViewState(name = "mockName-1"),
commonContentViewState = createCommonContentViewState(name = ""),
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Passport(),
)
val expectedValidationDialogState = passportState.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 = passportState,
@@ -2460,19 +2467,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
),
)
viewModel.eventFlow.test {
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
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())
assertEquals(passportState, stateFlow.awaitItem())
assertEquals(expectedValidationDialogState, stateFlow.awaitItem())
eventFlow.expectNoEvents()
}
}
@@ -2533,13 +2532,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
@Test
fun `ItemType Passport should expose PASSPORT itemTypeOption and not be SDK supported`() {
fun `ItemType Passport should expose PASSPORT itemTypeOption and be SDK supported`() {
val itemType = VaultAddEditState.ViewState.Content.ItemType.Passport()
assertEquals(
VaultAddEditState.ItemTypeOption.PASSPORT,
itemType.itemTypeOption,
)
assertFalse(itemType.isSdkSupported)
assertTrue(itemType.isSdkSupported)
assertTrue(itemType.vaultLinkedFieldTypes.isEmpty())
}
@@ -4340,6 +4339,188 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
}
@Nested
inner class VaultAddEditPassportTypeItemActions {
private lateinit var viewModel: VaultAddEditViewModel
private lateinit var vaultAddItemInitialState: VaultAddEditState
private lateinit var passportInitialSavedStateHandle: SavedStateHandle
@BeforeEach
fun setup() {
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(cipherListView = createMockCipherListView(1)),
)
vaultAddItemInitialState = createVaultAddItemState(
vaultItemCipherType = VaultItemCipherType.PASSPORT,
typeContentViewState =
VaultAddEditState.ViewState.Content.ItemType.Passport(),
)
passportInitialSavedStateHandle = createSavedStateHandleWithState(
state = vaultAddItemInitialState,
vaultAddEditType = VaultAddEditType.AddItem,
vaultItemCipherType = VaultItemCipherType.PASSPORT,
)
viewModel = createAddVaultItemViewModel(
savedStateHandle = passportInitialSavedStateHandle,
)
}
private fun expectedPassport(
block: VaultAddEditState.ViewState.Content.ItemType.Passport.() ->
VaultAddEditState.ViewState.Content.ItemType.Passport,
): VaultAddEditState =
createVaultAddItemState(
vaultItemCipherType = VaultItemCipherType.PASSPORT,
typeContentViewState = VaultAddEditState
.ViewState
.Content
.ItemType
.Passport()
.block(),
)
@Test
fun `GivenNameTextChange should update given name`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.GivenNameTextChange(
givenName = "Bruce",
),
)
assertEquals(
expectedPassport { copy(givenName = "Bruce") },
viewModel.stateFlow.value,
)
}
@Test
fun `SurnameTextChange should update surname`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.SurnameTextChange(
surname = "Wayne",
),
)
assertEquals(
expectedPassport { copy(surname = "Wayne") },
viewModel.stateFlow.value,
)
}
@Test
fun `SexTextChange should update sex`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.SexTextChange(sex = "M"),
)
assertEquals(
expectedPassport { copy(sex = "M") },
viewModel.stateFlow.value,
)
}
@Test
fun `BirthPlaceTextChange should update birth place`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange(
birthPlace = "Gotham City",
),
)
assertEquals(
expectedPassport { copy(birthPlace = "Gotham City") },
viewModel.stateFlow.value,
)
}
@Test
fun `NationalityTextChange should update nationality`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.NationalityTextChange(
nationality = "American",
),
)
assertEquals(
expectedPassport { copy(nationality = "American") },
viewModel.stateFlow.value,
)
}
@Test
fun `PassportNumberTextChange should update passport number`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange(
passportNumber = "X12345678",
),
)
assertEquals(
expectedPassport { copy(passportNumber = "X12345678") },
viewModel.stateFlow.value,
)
}
@Test
fun `PassportTypeTextChange should update passport type`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange(
passportType = "Regular",
),
)
assertEquals(
expectedPassport { copy(passportType = "Regular") },
viewModel.stateFlow.value,
)
}
@Test
fun `NationalIdentificationNumberTextChange should update national id number`() = runTest {
viewModel.trySendAction(
VaultAddEditAction
.ItemType
.PassportType
.NationalIdentificationNumberTextChange(
nationalIdentificationNumber = "987-65-4321",
),
)
assertEquals(
expectedPassport { copy(nationalIdentificationNumber = "987-65-4321") },
viewModel.stateFlow.value,
)
}
@Test
fun `IssuingCountryTextChange should update issuing country`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange(
country = "USA",
),
)
assertEquals(
expectedPassport { copy(issuingCountry = "USA") },
viewModel.stateFlow.value,
)
}
@Test
fun `IssuingAuthorityTextChange should update issuing authority`() = runTest {
viewModel.trySendAction(
VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange(
authority = "U.S. Department of State",
),
)
assertEquals(
expectedPassport { copy(issuingAuthority = "U.S. Department of State") },
viewModel.stateFlow.value,
)
}
}
@Test
fun `NumberVisibilityChange should log an event when in edit mode and password is visible`() =
runTest {

View File

@@ -9,6 +9,7 @@ import com.bitwarden.vault.FieldView
import com.bitwarden.vault.IdentityView
import com.bitwarden.vault.LoginUriView
import com.bitwarden.vault.LoginView
import com.bitwarden.vault.PassportView
import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SecureNoteView
@@ -554,6 +555,89 @@ class VaultAddItemStateExtensionsTest {
)
}
@Test
fun `toCipherView should transform Passport ItemType to CipherView`() {
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
name = "mockName-1",
selectedFolderId = "mockId-1",
favorite = false,
masterPasswordReprompt = false,
notes = "mockNotes-1",
selectedOwnerId = "mockOwnerId-1",
),
isIndividualVaultDisabled = false,
type = VaultAddEditState.ViewState.Content.ItemType.Passport(
givenName = "Bruce",
surname = "Wayne",
dateOfBirth = "1939-05-27",
sex = "M",
birthPlace = "Gotham City",
nationality = "American",
passportNumber = "X12345678",
passportType = "Regular",
nationalIdentificationNumber = "987-65-4321",
issuingCountry = "USA",
issuingAuthority = "U.S. Department of State",
issueDate = "2020-01-15",
expirationDate = "2030-01-15",
),
)
val result = viewState.toCipherView(clock = FIXED_CLOCK, isPremiumUser = true)
assertEquals(
CipherView(
id = null,
organizationId = "mockOwnerId-1",
folderId = "mockId-1",
collectionIds = emptyList(),
key = null,
name = "mockName-1",
notes = "mockNotes-1",
type = CipherType.PASSPORT,
login = null,
identity = null,
card = null,
secureNote = null,
bankAccount = null,
driversLicense = null,
passport = PassportView(
surname = "Wayne",
givenName = "Bruce",
dateOfBirth = "1939-05-27",
birthPlace = "Gotham City",
sex = "M",
nationality = "American",
passportNumber = "X12345678",
passportType = "Regular",
issuingCountry = "USA",
issuingAuthority = "U.S. Department of State",
issueDate = "2020-01-15",
expirationDate = "2030-01-15",
nationalIdentificationNumber = "987-65-4321",
),
favorite = false,
reprompt = CipherRepromptType.NONE,
organizationUseTotp = false,
edit = true,
viewPassword = true,
localData = null,
attachments = null,
fields = emptyList(),
passwordHistory = null,
permissions = null,
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = FIXED_CLOCK.instant(),
archivedDate = null,
sshKey = null,
attachmentDecryptionFailures = null,
),
result,
)
}
@Test
fun `toCipherView should transform Card ItemType to CipherView`() {
val viewState = VaultAddEditState.ViewState.Content(