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 56ef086193..59ed3aae25 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 @@ -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.VaultAddEditContent( sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers, bankAccountItemTypeHandlers: VaultAddEditBankAccountTypeHandlers, licenseItemTypeHandlers: VaultAddEditLicenseTypeHandlers, + passportItemTypeHandlers: VaultAddEditPassportTypeHandlers, isCardScannerEnabled: Boolean, cardHolderNameFocusRequester: FocusRequester, modifier: Modifier = Modifier, @@ -296,7 +298,13 @@ fun CoachMarkScope.VaultAddEditContent( licenseHandlers = licenseItemTypeHandlers, ) } - is VaultAddEditState.ViewState.Content.ItemType.Passport -> Unit + + is VaultAddEditState.ViewState.Content.ItemType.Passport -> { + vaultAddEditPassportItems( + passportState = state.type, + passportHandlers = passportItemTypeHandlers, + ) + } } vaultAddEditAdditionalOptions( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditPassportItems.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditPassportItems.kt new file mode 100644 index 0000000000..49c1d8586e --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditPassportItems.kt @@ -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(), + ) + } +} 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 d7150af964..d69b5f3890 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 @@ -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, 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 223e83ab51..3e028ea5fc 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 @@ -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 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. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditPassportTypeHandlers.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditPassportTypeHandlers.kt new file mode 100644 index 0000000000..56ad2a27db --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditPassportTypeHandlers.kt @@ -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) + } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt index 8d8e4bd23e..4ce32c8701 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt @@ -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? = 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 5b01ed4f96..0f9b472b2f 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 @@ -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( 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 d8bd7a1bfb..8b79b868e7 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 @@ -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 { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt index 070435323b..6e854ccfde 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt @@ -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(