BIT-511: Save identity items (#436)

This commit is contained in:
Ramsey Smith 2023-12-27 14:37:36 -07:00 committed by Álison Fernandes
parent f953066f22
commit a2e3984a5e
7 changed files with 329 additions and 19 deletions

View file

@ -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(

View file

@ -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.
*

View file

@ -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(

View file

@ -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(),
)
}

View file

@ -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`() {

View file

@ -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(

View file

@ -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",
),
)