mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-511: Save identity items (#436)
This commit is contained in:
parent
f953066f22
commit
a2e3984a5e
7 changed files with 329 additions and 19 deletions
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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`() {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue