diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditIdentityItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditIdentityItems.kt index f6e762442..d12a6e544 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditIdentityItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditIdentityItems.kt @@ -213,6 +213,17 @@ fun LazyListScope.addEditIdentityItems( .padding(horizontal = 16.dp), ) } + item { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.state_province), + value = identityState.state, + onValueChange = identityItemTypeHandlers.onStateTextChange, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } item { Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt index ea56e508d..b2ae06425 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt @@ -462,6 +462,10 @@ class VaultAddItemViewModel @Inject constructor( handleIdentityCityTextChange(action) } + is VaultAddItemAction.ItemType.IdentityType.StateTextChange -> { + handleIdentityStateTextChange(action) + } + is VaultAddItemAction.ItemType.IdentityType.CompanyTextChange -> { handleIdentityCompanyTextChange(action) } @@ -536,6 +540,12 @@ class VaultAddItemViewModel @Inject constructor( updateIdentityContent { it.copy(city = action.city) } } + private fun handleIdentityStateTextChange( + action: VaultAddItemAction.ItemType.IdentityType.StateTextChange, + ) { + updateIdentityContent { it.copy(state = action.state) } + } + private fun handleIdentityCompanyTextChange( action: VaultAddItemAction.ItemType.IdentityType.CompanyTextChange, ) { @@ -949,6 +959,7 @@ data class VaultAddItemState( * @property address2 The address2 for the identity item. * @property address3 The address3 for the identity item. * @property city The city for the identity item. + * @property state the state for the identity item. * @property zip The zip for the identity item. * @property country The country for the identity item. */ @@ -969,6 +980,7 @@ data class VaultAddItemState( val address2: String = "", val address3: String = "", val city: String = "", + val state: String = "", val zip: String = "", val country: String = "", ) : ItemType() { @@ -1350,6 +1362,13 @@ sealed class VaultAddItemAction { */ data class CityTextChange(val city: String) : IdentityType() + /** + * Fired when the state text input is changed. + * + * @property state The new state text. + */ + data class StateTextChange(val state: String) : IdentityType() + /** * Fired when the zip text input is changed. * diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/handlers/VaultAddIdentityItemTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/handlers/VaultAddIdentityItemTypeHandlers.kt index 31cd9c10d..90521f8a2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/handlers/VaultAddIdentityItemTypeHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/handlers/VaultAddIdentityItemTypeHandlers.kt @@ -42,6 +42,7 @@ class VaultAddIdentityItemTypeHandlers( val onAddress2TextChange: (String) -> Unit, val onAddress3TextChange: (String) -> Unit, val onCityTextChange: (String) -> Unit, + val onStateTextChange: (String) -> Unit, val onZipTextChange: (String) -> Unit, val onCountryTextChange: (String) -> Unit, ) { @@ -159,6 +160,13 @@ class VaultAddIdentityItemTypeHandlers( ), ) }, + onStateTextChange = { newState -> + viewModel.trySendAction( + VaultAddItemAction.ItemType.IdentityType.StateTextChange( + state = newState, + ), + ) + }, onZipTextChange = { newZip -> viewModel.trySendAction( VaultAddItemAction.ItemType.IdentityType.ZipTextChange( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt index 8c0d65be9..bc6155af2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt @@ -14,6 +14,7 @@ import com.bitwarden.core.SecureNoteView import com.bitwarden.core.UriMatchType import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank import com.x8bit.bitwarden.ui.vault.feature.additem.VaultAddItemState import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState import java.time.Instant @@ -160,26 +161,25 @@ private fun VaultAddItemState.ViewState.Content.ItemType.toCardView(): CardView? private fun VaultAddItemState.ViewState.Content.ItemType.toIdentityView(): IdentityView? = (this as? VaultAddItemState.ViewState.Content.ItemType.Identity)?.let { - // TODO Create real IdentityView from Content (BIT-508) IdentityView( - title = null, - firstName = null, - lastName = null, - middleName = null, - address1 = null, - address2 = null, - address3 = null, - city = null, - state = null, - postalCode = null, - country = null, - company = null, - email = null, - phone = null, - ssn = null, - username = null, - passportNumber = null, - licenseNumber = null, + title = it.selectedTitle.name, + firstName = it.firstName.orNullIfBlank(), + lastName = it.lastName.orNullIfBlank(), + middleName = it.middleName.orNullIfBlank(), + address1 = it.address1.orNullIfBlank(), + address2 = it.address2.orNullIfBlank(), + address3 = it.address3.orNullIfBlank(), + city = it.city.orNullIfBlank(), + state = it.state.orNullIfBlank(), + postalCode = it.zip.orNullIfBlank(), + country = it.country.orNullIfBlank(), + company = it.company.orNullIfBlank(), + email = it.email.orNullIfBlank(), + phone = it.phone.orNullIfBlank(), + ssn = it.ssn.orNullIfBlank(), + username = it.username.orNullIfBlank(), + passportNumber = it.passportNumber.orNullIfBlank(), + licenseNumber = it.licenseNumber.orNullIfBlank(), ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt index 105627760..5827f80a8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt @@ -952,6 +952,39 @@ class VaultAddItemScreenTest : BaseComposeTest() { } } + @Test + fun `in ItemType_Identity changing state text field should trigger CityTextChange`() { + mutableStateFlow.value = DEFAULT_STATE_IDENTITY + composeTestRule + .onNodeWithTextAfterScroll(text = "State / Province") + .performTextInput(text = "TestState") + + verify { + viewModel.trySendAction( + VaultAddItemAction.ItemType.IdentityType.StateTextChange( + state = "TestState", + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Identity the state province text field should display the text provided by the state`() { + mutableStateFlow.value = DEFAULT_STATE_IDENTITY + composeTestRule + .onNodeWithTextAfterScroll(text = "State / Province") + .assertTextContains("") + + mutableStateFlow.update { currentState -> + updateIdentityType(currentState) { copy(state = "NewState") } + } + + composeTestRule + .onNodeWithTextAfterScroll(text = "State / Province") + .assertTextContains("NewState") + } + @Suppress("MaxLineLength") @Test fun `in ItemType_Identity the country text field should display the text provided by the state`() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt index 8897479cc..0a888032e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt @@ -765,6 +765,21 @@ class VaultAddItemViewModelTest : BaseViewModelTest() { assertEquals(expectedState, viewModel.stateFlow.value) } + @Test + fun `StateTextChange should update state text`() = runTest { + val action = VaultAddItemAction.ItemType.IdentityType.StateTextChange( + state = "newState", + ) + val expectedState = createVaultAddItemState( + typeContentViewState = VaultAddItemState.ViewState.Content.ItemType.Identity( + state = "newState", + ), + ) + viewModel.actionChannel.trySend(action) + + assertEquals(expectedState, viewModel.stateFlow.value) + } + @Test fun `ZipTextChange should update zip`() = runTest { val action = VaultAddItemAction.ItemType.IdentityType.ZipTextChange( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt index 29210c92e..e1a1b771a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt @@ -5,6 +5,7 @@ import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView import com.bitwarden.core.FieldType import com.bitwarden.core.FieldView +import com.bitwarden.core.IdentityView import com.bitwarden.core.LoginUriView import com.bitwarden.core.LoginView import com.bitwarden.core.PasswordHistoryView @@ -386,6 +387,205 @@ class VaultDataExtensionsTest { result, ) } + + @Test + fun `toCipherView should transform Identity ItemType to CipherView`() { + mockkStatic(Instant::class) + every { Instant.now() } returns Instant.MIN + val viewState = VaultAddItemState.ViewState.Content( + common = VaultAddItemState.ViewState.Content.Common( + name = "mockName-1", + folderName = "mockFolder-1".asText(), + favorite = false, + masterPasswordReprompt = false, + notes = "mockNotes-1", + ownership = "mockOwnership-1", + ), + type = VaultAddItemState.ViewState.Content.ItemType.Identity( + selectedTitle = VaultAddItemState.ViewState.Content.ItemType.Identity.Title.MR, + firstName = "mockFirstName", + lastName = "mockLastName", + middleName = "mockMiddleName", + address1 = "mockAddress1", + address2 = "mockAddress2", + address3 = "mockAddress3", + city = "mockCity", + state = "mockState", + zip = "mockPostalCode", + country = "mockCountry", + company = "mockCompany", + email = "mockEmail", + phone = "mockPhone", + ssn = "mockSsn", + username = "MockUsername", + passportNumber = "mockPassportNumber", + licenseNumber = "mockLicenseNumber", + ), + ) + + val result = viewState.toCipherView() + + assertEquals( + CipherView( + id = null, + organizationId = null, + folderId = null, + collectionIds = emptyList(), + key = null, + name = "mockName-1", + notes = "mockNotes-1", + type = CipherType.IDENTITY, + login = null, + identity = IdentityView( + title = "MR", + firstName = "mockFirstName", + lastName = "mockLastName", + middleName = "mockMiddleName", + address1 = "mockAddress1", + address2 = "mockAddress2", + address3 = "mockAddress3", + city = "mockCity", + state = "mockState", + postalCode = "mockPostalCode", + country = "mockCountry", + company = "mockCompany", + email = "mockEmail", + phone = "mockPhone", + ssn = "mockSsn", + username = "MockUsername", + passportNumber = "mockPassportNumber", + licenseNumber = "mockLicenseNumber", + ), + card = null, + secureNote = null, + favorite = false, + reprompt = CipherRepromptType.NONE, + organizationUseTotp = false, + edit = true, + viewPassword = true, + localData = null, + attachments = null, + fields = emptyList(), + passwordHistory = null, + creationDate = Instant.MIN, + deletedDate = null, + revisionDate = Instant.MIN, + ), + result, + ) + } + + @Test + fun `toCipherView should transform Identity ItemType to CipherView with original cipher`() { + val cipherView = DEFAULT_IDENTITY_CIPHER_VIEW + val viewState = VaultAddItemState.ViewState.Content( + common = VaultAddItemState.ViewState.Content.Common( + originalCipher = cipherView, + name = "mockName-1", + folderName = "mockFolder-1".asText(), + favorite = true, + masterPasswordReprompt = false, + customFieldData = listOf( + VaultAddItemState.Custom.BooleanField("testId", "TestBoolean", false), + VaultAddItemState.Custom.TextField("testId", "TestText", "TestText"), + VaultAddItemState.Custom.HiddenField("testId", "TestHidden", "TestHidden"), + VaultAddItemState.Custom.LinkedField( + "testId", + "TestLinked", + VaultLinkedFieldType.USERNAME, + ), + ), + notes = "mockNotes-1", + ownership = "mockOwnership-1", + ), + type = VaultAddItemState.ViewState.Content.ItemType.Identity( + selectedTitle = VaultAddItemState.ViewState.Content.ItemType.Identity.Title.MR, + firstName = "mockFirstName", + lastName = "mockLastName", + middleName = "mockMiddleName", + address1 = "mockAddress1", + address2 = "mockAddress2", + address3 = "mockAddress3", + city = "mockCity", + state = "mockState", + zip = "mockPostalCode", + country = "mockCountry", + company = "mockCompany", + email = "mockEmail", + phone = "mockPhone", + ssn = "mockSsn", + username = "MockUsername", + passportNumber = "mockPassportNumber", + licenseNumber = "mockLicenseNumber", + ), + ) + + val result = viewState.toCipherView() + + assertEquals( + @Suppress("MaxLineLength") + cipherView.copy( + name = "mockName-1", + notes = "mockNotes-1", + type = CipherType.IDENTITY, + identity = IdentityView( + title = "MR", + firstName = "mockFirstName", + lastName = "mockLastName", + middleName = "mockMiddleName", + address1 = "mockAddress1", + address2 = "mockAddress2", + address3 = "mockAddress3", + city = "mockCity", + state = "mockState", + postalCode = "mockPostalCode", + country = "mockCountry", + company = "mockCompany", + email = "mockEmail", + phone = "mockPhone", + ssn = "mockSsn", + username = "MockUsername", + passportNumber = "mockPassportNumber", + licenseNumber = "mockLicenseNumber", + ), + favorite = true, + reprompt = CipherRepromptType.NONE, + fields = listOf( + FieldView( + name = "TestBoolean", + value = "false", + type = FieldType.BOOLEAN, + linkedId = null, + ), + FieldView( + name = "TestText", + value = "TestText", + type = FieldType.TEXT, + linkedId = null, + ), + FieldView( + name = "TestHidden", + value = "TestHidden", + type = FieldType.HIDDEN, + linkedId = null, + ), + FieldView( + name = "TestLinked", + value = null, + type = FieldType.LINKED, + linkedId = VaultLinkedFieldType.USERNAME.id, + ), + ), + passwordHistory = listOf( + PasswordHistoryView( + password = "old_password", + lastUsedDate = Instant.ofEpochSecond(1_000L), + ), + ), + ), + result, + ) + } } private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView( @@ -492,3 +692,27 @@ private val DEFAULT_SECURE_NOTES_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_V ), secureNote = SecureNoteView(type = SecureNoteType.GENERIC), ) + +private val DEFAULT_IDENTITY_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy( + type = CipherType.IDENTITY, + identity = IdentityView( + title = "MR", + firstName = "mockFirstName", + lastName = "mockLastName", + middleName = "mockMiddleName", + address1 = "mockAddress1", + address2 = "mockAddress2", + address3 = "mockAddress3", + city = "mockCity", + state = "mockState", + postalCode = "mockPostalCode", + country = "mockCountry", + company = "mockCompany", + email = "mockEmail", + phone = "mockPhone", + ssn = "mockSsn", + username = "MockUsername", + passportNumber = "mockPassportNumber", + licenseNumber = "mockLicenseNumber", + ), +)