BIT-514: View identity item UI (#461)

This commit is contained in:
Ramsey Smith 2024-01-02 14:09:58 -06:00 committed by Álison Fernandes
parent cd1f703ba7
commit 0c05855e6b
16 changed files with 1066 additions and 369 deletions

View file

@ -0,0 +1,12 @@
package com.x8bit.bitwarden.ui.platform.base.util
/**
* Returns null if all entries in a given list are equal to a provided [value], otherwise
* the original list is returned.
*/
fun <T> List<T>.nullIfAllEqual(value: T): List<T>? =
if (all { it == value }) {
null
} else {
this
}

View file

@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")
package com.x8bit.bitwarden.ui.platform.base.util
import androidx.compose.runtime.Composable
@ -154,3 +156,10 @@ fun String.toHexColorRepresentation(): String {
val blue = ((hash and 0xFF0000) shr 16).toTwoDigitHexString()
return "#ff$red$green$blue"
}
/**
* Returns a copy of this string having its first letter titlecased using the rules of the specified
* [locale], or the original string if it's empty or already starts with a title case letter.
*/
fun String.capitalize(locale: Locale = Locale.getDefault()): String =
replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() }

View file

@ -28,6 +28,7 @@ fun CipherView.toViewState(): VaultAddItemState.ViewState =
CipherType.SECURE_NOTE -> VaultAddItemState.ViewState.Content.ItemType.SecureNotes
CipherType.CARD -> VaultAddItemState.ViewState.Content.ItemType.Card
CipherType.IDENTITY -> VaultAddItemState.ViewState.Content.ItemType.Identity(
selectedTitle = identity?.title.toTitleOrDefault(),
firstName = identity?.firstName.orEmpty(),
middleName = identity?.middleName.orEmpty(),
lastName = identity?.lastName.orEmpty(),
@ -89,3 +90,10 @@ private fun FieldView.toCustomField() =
vaultLinkedFieldType = fromId(requireNotNull(this.linkedId)),
)
}
@Suppress("MaxLineLength")
private fun String?.toTitleOrDefault(): VaultAddItemState.ViewState.Content.ItemType.Identity.Title =
VaultAddItemState.ViewState.Content.ItemType.Identity.Title
.entries
.find { it.name == this }
?: VaultAddItemState.ViewState.Content.ItemType.Identity.Title.MR

View file

@ -0,0 +1,101 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
/**
* Custom Field UI common for all item types.
*/
@Suppress("LongMethod", "MaxLineLength")
@Composable
fun CustomField(
customField: VaultItemState.ViewState.Content.Common.Custom,
onCopyCustomHiddenField: (String) -> Unit,
onCopyCustomTextField: (String) -> Unit,
onShowHiddenFieldClick: (VaultItemState.ViewState.Content.Common.Custom.HiddenField, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
when (customField) {
is VaultItemState.ViewState.Content.Common.Custom.BooleanField -> {
BitwardenWideSwitch(
label = customField.name,
isChecked = customField.value,
readOnly = true,
onCheckedChange = { },
modifier = modifier,
)
}
is VaultItemState.ViewState.Content.Common.Custom.HiddenField -> {
BitwardenPasswordFieldWithActions(
label = customField.name,
value = customField.value,
showPasswordChange = { onShowHiddenFieldClick(customField, it) },
showPassword = customField.isVisible,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
actions = {
if (customField.isCopyable) {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_copy),
contentDescription = stringResource(id = R.string.copy),
),
onClick = {
onCopyCustomHiddenField(customField.value)
},
)
}
},
)
}
is VaultItemState.ViewState.Content.Common.Custom.LinkedField -> {
BitwardenTextField(
label = customField.name,
value = customField.vaultLinkedFieldType.label.invoke(),
leadingIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_linked),
contentDescription = stringResource(id = R.string.field_type_linked),
),
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
)
}
is VaultItemState.ViewState.Content.Common.Custom.TextField -> {
BitwardenTextFieldWithActions(
label = customField.name,
value = customField.value,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
actions = {
if (customField.isCopyable) {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_copy),
contentDescription = stringResource(id = R.string.copy),
),
onClick = { onCopyCustomTextField(customField.value) },
)
}
},
)
}
}
}

View file

@ -0,0 +1,227 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
/**
* The top level content UI state for the [VaultItemScreen] when viewing a Identity cipher.
*/
@Suppress("LongMethod")
@Composable
fun VaultItemIdentityContent(
identityState: VaultItemState.ViewState.Content.ItemType.Identity,
commonState: VaultItemState.ViewState.Content.Common,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
identityState.identityName?.let { identityName ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.identity_name),
value = identityName,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.username?.let { username ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.username),
value = username,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.company?.let { company ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.company),
value = company,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.ssn?.let { ssn ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.ssn),
value = ssn,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.passportNumber?.let { passportNumber ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.passport_number),
value = passportNumber,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.licenseNumber?.let { licenseNumber ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.license_number),
value = licenseNumber,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.email?.let { email ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.email),
value = email,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.phone?.let { phone ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.phone),
value = phone,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
identityState.address?.let { address ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.address),
value = address,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(4.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}

View file

@ -23,7 +23,6 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
@ -172,7 +171,7 @@ fun VaultItemLoginContent(
item {
Spacer(modifier = Modifier.height(24.dp))
UpdateText(
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
@ -183,7 +182,7 @@ fun VaultItemLoginContent(
loginItemState.passwordRevisionDate?.let { revisionDate ->
item {
UpdateText(
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_password_updated)}: ",
text = revisionDate,
modifier = Modifier
@ -212,91 +211,6 @@ fun VaultItemLoginContent(
}
}
@Suppress("LongMethod", "MaxLineLength")
@Composable
private fun CustomField(
customField: VaultItemState.ViewState.Content.Common.Custom,
onCopyCustomHiddenField: (String) -> Unit,
onCopyCustomTextField: (String) -> Unit,
onShowHiddenFieldClick: (VaultItemState.ViewState.Content.Common.Custom.HiddenField, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
when (customField) {
is VaultItemState.ViewState.Content.Common.Custom.BooleanField -> {
BitwardenWideSwitch(
label = customField.name,
isChecked = customField.value,
readOnly = true,
onCheckedChange = { },
modifier = modifier,
)
}
is VaultItemState.ViewState.Content.Common.Custom.HiddenField -> {
BitwardenPasswordFieldWithActions(
label = customField.name,
value = customField.value,
showPasswordChange = { onShowHiddenFieldClick(customField, it) },
showPassword = customField.isVisible,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
actions = {
if (customField.isCopyable) {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_copy),
contentDescription = stringResource(id = R.string.copy),
),
onClick = {
onCopyCustomHiddenField(customField.value)
},
)
}
},
)
}
is VaultItemState.ViewState.Content.Common.Custom.LinkedField -> {
BitwardenTextField(
label = customField.name,
value = customField.vaultLinkedFieldType.label.invoke(),
leadingIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_linked),
contentDescription = stringResource(id = R.string.field_type_linked),
),
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
)
}
is VaultItemState.ViewState.Content.Common.Custom.TextField -> {
BitwardenTextFieldWithActions(
label = customField.name,
value = customField.value,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
actions = {
if (customField.isCopyable) {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_copy),
contentDescription = stringResource(id = R.string.copy),
),
onClick = { onCopyCustomTextField(customField.value) },
)
}
},
)
}
}
}
@Composable
private fun NotesField(
notes: String,
@ -393,29 +307,6 @@ private fun TotpField(
}
}
@Composable
private fun UpdateText(
header: String,
text: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.semantics(mergeDescendants = true) { },
) {
Text(
text = header,
style = LocalNonMaterialTypography.current.labelMediumProminent,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun UriField(
uriData: VaultItemState.ViewState.Content.ItemType.Login.UriData,

View file

@ -197,9 +197,9 @@ private fun VaultItemContent(
VaultItemLoginContent(
commonState = viewState.common,
loginItemState = viewState.type,
modifier = modifier,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
vaultLoginItemTypeHandlers = vaultLoginItemTypeHandlers,
modifier = modifier,
)
}
@ -208,7 +208,12 @@ private fun VaultItemContent(
}
is VaultItemState.ViewState.Content.ItemType.Identity -> {
// TODO UI for viewing Identity BIT-514
VaultItemIdentityContent(
commonState = viewState.common,
identityState = viewState.type,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
modifier = modifier,
)
}
is VaultItemState.ViewState.Content.ItemType.SecureNote -> {

View file

@ -0,0 +1,35 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
/**
* Update Text UI common for all item types.
*/
@Composable
fun VaultItemUpdateText(
header: String,
text: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.semantics(mergeDescendants = true) { },
) {
Text(
text = header,
style = LocalNonMaterialTypography.current.labelMediumProminent,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

View file

@ -612,8 +612,28 @@ data class VaultItemState(
/**
* Represents the `Identity` item type.
*
* @property identityName The name for the identity.
* @property username The username for the identity.
* @property company The company associated with the identity.
* @property ssn The SSN for the identity.
* @property passportNumber The passport number for the identity.
* @property licenseNumber The license number for the identity.
* @property email The email for the identity.
* @property phone The phone number for the identity.
* @property address The address for the identity.
*/
data object Identity : ItemType()
data class Identity(
val identityName: String?,
val username: String?,
val company: String?,
val ssn: String?,
val passportNumber: String?,
val licenseNumber: String?,
val email: String?,
val phone: String?,
val address: String?,
) : ItemType()
/**
* Represents the `Card` item type.

View file

@ -5,8 +5,12 @@ 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.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.capitalize
import com.x8bit.bitwarden.ui.platform.base.util.nullIfAllEqual
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.platform.base.util.orZeroWidthSpace
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
@ -63,7 +67,17 @@ fun CipherView.toViewState(
}
CipherType.IDENTITY -> {
VaultItemState.ViewState.Content.ItemType.Identity
VaultItemState.ViewState.Content.ItemType.Identity(
username = identity?.username,
identityName = identity?.identityName,
company = identity?.company,
ssn = identity?.ssn,
passportNumber = identity?.passportNumber,
licenseNumber = identity?.licenseNumber,
email = identity?.email,
phone = identity?.phone,
address = identity?.identityAddress,
)
}
},
)
@ -100,3 +114,28 @@ private fun LoginUriView.toUriData() =
isCopyable = !uri.isNullOrBlank(),
isLaunchable = !uri.isNullOrBlank(),
)
private val IdentityView.identityAddress: String?
get() = listOfNotNull(
address1,
address2,
address3,
listOf(city ?: "-", state ?: "-", postalCode ?: "-")
.nullIfAllEqual("-")
?.joinToString(", "),
country,
)
.joinToString("\n")
.orNullIfBlank()
private val IdentityView.identityName: String?
get() = listOfNotNull(
title
?.lowercase()
?.capitalize(),
firstName,
middleName,
lastName,
)
.joinToString(" ")
.orNullIfBlank()

View file

@ -0,0 +1,29 @@
package com.x8bit.bitwarden.ui.platform.base.util
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
class ListExtensionsTest {
@Test
fun `nullIfAllEqual should return null for lists with identical values`() {
val initialList = listOf("-", "-", "-", "-", "-")
val result = initialList.nullIfAllEqual("-")
assertNull(result)
}
@Test
fun `nullIfAllEqual should return the initial list for lists with non-identical values`() {
val initialList = listOf("-", "-", "-", "-", "1")
val result = initialList.nullIfAllEqual("-")
assertEquals(
initialList,
result,
)
}
}

View file

@ -86,4 +86,16 @@ class StringExtensionsTest {
)
}
}
@Test
fun `capitalize should return a capitalized string`() {
val initialString = "lowercase"
val result = initialString.capitalize()
assertEquals(
"Lowercase",
result,
)
}
}

View file

@ -752,6 +752,123 @@ class VaultItemScreenTest : BaseComposeTest() {
composeTestRule.assertScrollableNodeDoesNotExist("Password history: ")
composeTestRule.assertScrollableNodeDoesNotExist("1")
}
@Test
fun `in identity state, identityName should be displayed according to state`() {
val identityName = "the identity name"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(identityName = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, username should be displayed according to state`() {
val identityName = "the username"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(username = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, company should be displayed according to state`() {
val identityName = "the company name"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(company = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, ssn should be displayed according to state`() {
val identityName = "the SSN"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(ssn = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, passportNumber should be displayed according to state`() {
val identityName = "the passport number"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(passportNumber = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, licenseNumber should be displayed according to state`() {
val identityName = "the license number"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(licenseNumber = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, email should be displayed according to state`() {
val identityName = "the email address"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(email = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, phone should be displayed according to state`() {
val identityName = "the phone number"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(phone = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
@Test
fun `in identity state, address should be displayed according to state`() {
val identityName = "the address"
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll(identityName).assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateIdentityType(currentState) { copy(address = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
}
//region Helper functions
@ -780,6 +897,28 @@ private fun updateLoginType(
return currentState.copy(viewState = updatedType)
}
@Suppress("MaxLineLength")
private fun updateIdentityType(
currentState: VaultItemState,
transform: VaultItemState.ViewState.Content.ItemType.Identity.() ->
VaultItemState.ViewState.Content.ItemType.Identity,
): VaultItemState {
val updatedType = when (val viewState = currentState.viewState) {
is VaultItemState.ViewState.Content -> {
when (val type = viewState.type) {
is VaultItemState.ViewState.Content.ItemType.Identity -> {
viewState.copy(
type = type.transform(),
)
}
else -> viewState
}
}
else -> viewState
}
return currentState.copy(viewState = updatedType)
}
@Suppress("MaxLineLength")
private fun updateCommonContent(
currentState: VaultItemState,
@ -805,57 +944,70 @@ private val DEFAULT_STATE: VaultItemState = VaultItemState(
dialog = null,
)
private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
type = VaultItemState.ViewState.Content.ItemType.Login(
passwordHistoryCount = 1,
username = "the username",
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
password = "the password",
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
lastUpdated = "12/31/69 06:16 PM",
name = "login cipher",
notes = "Lots of notes",
isPremiumUser = true,
customFields = listOf(
VaultItemState.ViewState.Content.Common.Custom.TextField(
name = "text",
value = "value",
isCopyable = true,
),
VaultItemState.ViewState.Content.Common.Custom.HiddenField(
name = "hidden",
value = "hidden password",
isCopyable = true,
isVisible = false,
),
uris = listOf(
VaultItemState.ViewState.Content.ItemType.Login.UriData(
uri = "www.example.com",
isCopyable = true,
isLaunchable = true,
),
VaultItemState.ViewState.Content.Common.Custom.BooleanField(
name = "boolean",
value = true,
),
passwordRevisionDate = "4/14/83 3:56 PM",
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
),
common = VaultItemState.ViewState.Content.Common(
lastUpdated = "12/31/69 06:16 PM",
name = "login cipher",
notes = "Lots of notes",
isPremiumUser = true,
customFields = listOf(
VaultItemState.ViewState.Content.Common.Custom.TextField(
name = "text",
value = "value",
isCopyable = true,
),
VaultItemState.ViewState.Content.Common.Custom.HiddenField(
name = "hidden",
value = "hidden password",
isCopyable = true,
isVisible = false,
),
VaultItemState.ViewState.Content.Common.Custom.BooleanField(
name = "boolean",
value = true,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked username",
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked password",
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked username",
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked password",
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
),
requiresReprompt = true,
),
requiresReprompt = true,
)
private val DEFAULT_LOGIN: VaultItemState.ViewState.Content.ItemType.Login =
VaultItemState.ViewState.Content.ItemType.Login(
passwordHistoryCount = 1,
username = "the username",
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
password = "the password",
isVisible = false,
),
uris = listOf(
VaultItemState.ViewState.Content.ItemType.Login.UriData(
uri = "www.example.com",
isCopyable = true,
isLaunchable = true,
),
),
passwordRevisionDate = "4/14/83 3:56 PM",
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
)
private val DEFAULT_IDENTITY: VaultItemState.ViewState.Content.ItemType.Identity =
VaultItemState.ViewState.Content.ItemType.Identity(
username = "the username",
identityName = "the identity name",
company = "the company name",
ssn = "the SSN",
passportNumber = "the passport number",
licenseNumber = "the license number",
email = "the email address",
phone = "the phone number",
address = "the address",
)
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
@ -883,3 +1035,15 @@ private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
common = EMPTY_COMMON,
type = EMPTY_LOGIN_TYPE,
)
private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
type = DEFAULT_LOGIN,
common = DEFAULT_COMMON,
)
private val DEFAULT_IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
type = DEFAULT_IDENTITY,
common = DEFAULT_COMMON,
)

View file

@ -12,7 +12,8 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.item.util.DEFAULT_EMPTY_LOGIN_VIEW_STATE
import com.x8bit.bitwarden.ui.vault.feature.item.util.createCommonContent
import com.x8bit.bitwarden.ui.vault.feature.item.util.createLoginContent
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import io.mockk.coEvery
@ -294,11 +295,12 @@ class VaultItemViewModelTest : BaseViewModelTest() {
isCopyable = true,
isVisible = false,
)
val loginViewState = DEFAULT_EMPTY_LOGIN_VIEW_STATE.copy(
common = DEFAULT_EMPTY_LOGIN_VIEW_STATE.common.copy(
val loginViewState = VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = true).copy(
requiresReprompt = false,
customFields = listOf(hiddenField),
),
type = createLoginContent(isEmpty = true),
)
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
val mockCipherView = mockk<CipherView> {
@ -316,7 +318,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
assertEquals(
loginState.copy(
viewState = loginViewState.copy(
common = DEFAULT_EMPTY_LOGIN_VIEW_STATE.common.copy(
common = createCommonContent(isEmpty = true).copy(
requiresReprompt = false,
customFields = listOf(hiddenField.copy(isVisible = true)),
),

View file

@ -1,20 +1,11 @@
package com.x8bit.bitwarden.ui.vault.feature.item.util
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.FieldType
import com.bitwarden.core.FieldView
import com.bitwarden.core.LoginUriView
import com.bitwarden.core.LoginView
import com.bitwarden.core.PasswordHistoryView
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Instant
import java.util.TimeZone
class CipherViewExtensionsTest {
@ -33,20 +24,29 @@ class CipherViewExtensionsTest {
@Test
fun `toViewState should transform full CipherView into ViewState Login Content with premium`() {
val viewState = DEFAULT_FULL_LOGIN_CIPHER_VIEW.toViewState(isPremiumUser = true)
val viewState = createCipherView(type = CipherType.LOGIN, isEmpty = false)
.toViewState(isPremiumUser = true)
assertEquals(DEFAULT_FULL_LOGIN_VIEW_STATE, viewState)
assertEquals(
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = false),
type = createLoginContent(isEmpty = false),
),
viewState,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should transform full CipherView into ViewState Login Content without premium`() {
val isPremiumUser = false
val viewState = DEFAULT_FULL_LOGIN_CIPHER_VIEW.toViewState(isPremiumUser = isPremiumUser)
val viewState = createCipherView(type = CipherType.LOGIN, isEmpty = false)
.toViewState(isPremiumUser = isPremiumUser)
assertEquals(
DEFAULT_FULL_LOGIN_VIEW_STATE.copy(
common = DEFAULT_FULL_LOGIN_VIEW_STATE.common.copy(isPremiumUser = isPremiumUser),
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = false).copy(isPremiumUser = isPremiumUser),
type = createLoginContent(isEmpty = false),
),
viewState,
)
@ -54,196 +54,120 @@ class CipherViewExtensionsTest {
@Test
fun `toViewState should transform empty CipherView into ViewState Login Content`() {
val viewState = DEFAULT_EMPTY_LOGIN_CIPHER_VIEW.toViewState(isPremiumUser = true)
val viewState = createCipherView(type = CipherType.LOGIN, isEmpty = true)
.toViewState(isPremiumUser = true)
assertEquals(DEFAULT_EMPTY_LOGIN_VIEW_STATE, viewState)
assertEquals(
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = true),
type = createLoginContent(isEmpty = true),
),
viewState,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should transform full CipherView into ViewState Identity Content with premium`() {
val viewState = createCipherView(type = CipherType.IDENTITY, isEmpty = false)
.toViewState(isPremiumUser = true)
assertEquals(
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = false),
type = createIdentityContent(isEmpty = false),
),
viewState,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should transform full CipherView into ViewState Identity Content without premium`() {
val isPremiumUser = false
val viewState = createCipherView(type = CipherType.IDENTITY, isEmpty = false)
.toViewState(isPremiumUser = isPremiumUser)
assertEquals(
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = false).copy(isPremiumUser = isPremiumUser),
type = createIdentityContent(isEmpty = false),
),
viewState,
)
}
@Test
fun `toViewState should transform empty CipherView into ViewState Identity Content`() {
val viewState = createCipherView(type = CipherType.IDENTITY, isEmpty = true)
.toViewState(isPremiumUser = true)
assertEquals(
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = true),
type = createIdentityContent(isEmpty = true),
),
viewState,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should transform CipherView with odd naming into ViewState Identity Content`() {
val viewState = createCipherView(type = CipherType.IDENTITY, isEmpty = false)
val result = viewState
.copy(
identity = viewState.identity?.copy(
title = "MX",
firstName = null,
middleName = "middleName",
lastName = null,
),
)
.toViewState(isPremiumUser = true)
assertEquals(
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = false),
type = createIdentityContent(
isEmpty = false,
identityName = "Mx middleName",
),
),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should transform CipherView with odd address into ViewState Identity Content`() {
val viewState = createCipherView(type = CipherType.IDENTITY, isEmpty = false)
val result = viewState
.copy(
identity = viewState.identity?.copy(
address1 = null,
address2 = null,
address3 = "address3",
city = null,
state = "state",
postalCode = null,
country = null,
),
)
.toViewState(isPremiumUser = true)
assertEquals(
VaultItemState.ViewState.Content(
common = createCommonContent(isEmpty = false),
type = createIdentityContent(
isEmpty = false,
address = """
address3
-, state, -
""".trimIndent(),
),
),
result,
)
}
}
val DEFAULT_FULL_LOGIN_VIEW: LoginView = LoginView(
username = "username",
password = "password",
passwordRevisionDate = Instant.ofEpochSecond(1_000L),
uris = listOf(
LoginUriView(
uri = "www.example.com",
match = null,
),
),
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
autofillOnPageLoad = false,
)
val DEFAULT_EMPTY_LOGIN_VIEW: LoginView = LoginView(
username = null,
password = null,
passwordRevisionDate = null,
uris = emptyList(),
totp = null,
autofillOnPageLoad = false,
)
val DEFAULT_FULL_LOGIN_CIPHER_VIEW: CipherView = CipherView(
id = null,
organizationId = null,
folderId = null,
collectionIds = emptyList(),
key = null,
name = "login cipher",
notes = "Lots of notes",
type = CipherType.LOGIN,
login = DEFAULT_FULL_LOGIN_VIEW,
identity = null,
card = null,
secureNote = null,
favorite = false,
reprompt = CipherRepromptType.PASSWORD,
organizationUseTotp = false,
edit = false,
viewPassword = false,
localData = null,
attachments = null,
fields = listOf(
FieldView(
name = "text",
value = "value",
type = FieldType.TEXT,
linkedId = null,
),
FieldView(
name = "hidden",
value = "value",
type = FieldType.HIDDEN,
linkedId = null,
),
FieldView(
name = "boolean",
value = "true",
type = FieldType.BOOLEAN,
linkedId = null,
),
FieldView(
name = "linked username",
value = null,
type = FieldType.LINKED,
linkedId = 100U,
),
FieldView(
name = "linked password",
value = null,
type = FieldType.LINKED,
linkedId = 101U,
),
),
passwordHistory = listOf(
PasswordHistoryView(
password = "old_password",
lastUsedDate = Instant.ofEpochSecond(1_000L),
),
),
creationDate = Instant.ofEpochSecond(1_000L),
deletedDate = null,
revisionDate = Instant.ofEpochSecond(1_000L),
)
val DEFAULT_EMPTY_LOGIN_CIPHER_VIEW: CipherView = CipherView(
id = null,
organizationId = null,
folderId = null,
collectionIds = emptyList(),
key = null,
name = "login cipher",
notes = null,
type = CipherType.LOGIN,
login = DEFAULT_EMPTY_LOGIN_VIEW,
identity = null,
card = null,
secureNote = null,
favorite = false,
reprompt = CipherRepromptType.PASSWORD,
organizationUseTotp = false,
edit = false,
viewPassword = false,
localData = null,
attachments = null,
fields = null,
passwordHistory = null,
creationDate = Instant.ofEpochSecond(1_000L),
deletedDate = null,
revisionDate = Instant.ofEpochSecond(1_000L),
)
val DEFAULT_FULL_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
common = VaultItemState.ViewState.Content.Common(
name = "login cipher",
lastUpdated = "1/1/70 12:16 AM",
notes = "Lots of notes",
isPremiumUser = true,
customFields = listOf(
VaultItemState.ViewState.Content.Common.Custom.TextField(
name = "text",
value = "value",
isCopyable = true,
),
VaultItemState.ViewState.Content.Common.Custom.HiddenField(
name = "hidden",
value = "value",
isCopyable = true,
isVisible = false,
),
VaultItemState.ViewState.Content.Common.Custom.BooleanField(
name = "boolean",
value = true,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked username",
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked password",
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
),
),
requiresReprompt = true,
),
type = VaultItemState.ViewState.Content.ItemType.Login(
passwordHistoryCount = 1,
username = "username",
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
password = "password",
isVisible = false,
),
uris = listOf(
VaultItemState.ViewState.Content.ItemType.Login.UriData(
uri = "www.example.com",
isCopyable = true,
isLaunchable = true,
),
),
passwordRevisionDate = "1/1/70 12:16 AM",
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
),
)
val DEFAULT_EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
common = VaultItemState.ViewState.Content.Common(
name = "login cipher",
lastUpdated = "1/1/70 12:16 AM",
notes = null,
isPremiumUser = true,
customFields = emptyList(),
requiresReprompt = true,
),
type = VaultItemState.ViewState.Content.ItemType.Login(
passwordHistoryCount = null,
username = null,
passwordData = null,
uris = emptyList(),
passwordRevisionDate = null,
totp = null,
),
)

View file

@ -0,0 +1,219 @@
package com.x8bit.bitwarden.ui.vault.feature.item.util
import com.bitwarden.core.CipherRepromptType
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
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import java.time.Instant
const val DEFAULT_IDENTITY_NAME: String = "Mr firstName middleName lastName"
val DEFAULT_ADDRESS: String =
"""
address1
address2
address3
city, state, postalCode
country
"""
.trimIndent()
fun createLoginView(isEmpty: Boolean): LoginView =
LoginView(
username = "username".takeUnless { isEmpty },
password = "password".takeUnless { isEmpty },
passwordRevisionDate = Instant.ofEpochSecond(1_000L).takeUnless { isEmpty },
uris = listOf(
LoginUriView(
uri = "www.example.com",
match = null,
),
)
.takeUnless { isEmpty },
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
.takeUnless { isEmpty },
autofillOnPageLoad = false,
)
@Suppress("CyclomaticComplexMethod")
fun createIdentityView(isEmpty: Boolean): IdentityView =
IdentityView(
title = "MR".takeUnless { isEmpty },
firstName = "firstName".takeUnless { isEmpty },
lastName = "lastName".takeUnless { isEmpty },
middleName = "middleName".takeUnless { isEmpty },
address1 = "address1".takeUnless { isEmpty },
address2 = "address2".takeUnless { isEmpty },
address3 = "address3".takeUnless { isEmpty },
city = "city".takeUnless { isEmpty },
state = "state".takeUnless { isEmpty },
postalCode = "postalCode".takeUnless { isEmpty },
country = "country".takeUnless { isEmpty },
company = "company".takeUnless { isEmpty },
email = "email".takeUnless { isEmpty },
phone = "phone".takeUnless { isEmpty },
ssn = "ssn".takeUnless { isEmpty },
username = "username".takeUnless { isEmpty },
passportNumber = "passportNumber".takeUnless { isEmpty },
licenseNumber = "licenseNumber".takeUnless { isEmpty },
)
fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView =
CipherView(
id = null,
organizationId = null,
folderId = null,
collectionIds = emptyList(),
key = null,
name = "mockName",
notes = "Lots of notes".takeUnless { isEmpty },
type = type,
login = createLoginView(isEmpty = isEmpty),
identity = createIdentityView(isEmpty = isEmpty),
card = null,
secureNote = null,
favorite = false,
reprompt = CipherRepromptType.PASSWORD,
organizationUseTotp = false,
edit = false,
viewPassword = false,
localData = null,
attachments = null,
fields = listOf(
FieldView(
name = "text",
value = "value",
type = FieldType.TEXT,
linkedId = null,
),
FieldView(
name = "hidden",
value = "value",
type = FieldType.HIDDEN,
linkedId = null,
),
FieldView(
name = "boolean",
value = "true",
type = FieldType.BOOLEAN,
linkedId = null,
),
FieldView(
name = "linked username",
value = null,
type = FieldType.LINKED,
linkedId = 100U,
),
FieldView(
name = "linked password",
value = null,
type = FieldType.LINKED,
linkedId = 101U,
),
)
.takeUnless { isEmpty },
passwordHistory = listOf(
PasswordHistoryView(
password = "old_password",
lastUsedDate = Instant.ofEpochSecond(1_000L),
),
)
.takeUnless { isEmpty },
creationDate = Instant.ofEpochSecond(1_000L),
deletedDate = null,
revisionDate = Instant.ofEpochSecond(1_000L),
)
fun createCommonContent(isEmpty: Boolean): VaultItemState.ViewState.Content.Common =
if (isEmpty) {
VaultItemState.ViewState.Content.Common(
name = "mockName",
lastUpdated = "1/1/70 12:16 AM",
notes = null,
isPremiumUser = true,
customFields = emptyList(),
requiresReprompt = true,
)
} else {
VaultItemState.ViewState.Content.Common(
name = "mockName",
lastUpdated = "1/1/70 12:16 AM",
notes = "Lots of notes",
isPremiumUser = true,
customFields = listOf(
VaultItemState.ViewState.Content.Common.Custom.TextField(
name = "text",
value = "value",
isCopyable = true,
),
VaultItemState.ViewState.Content.Common.Custom.HiddenField(
name = "hidden",
value = "value",
isCopyable = true,
isVisible = false,
),
VaultItemState.ViewState.Content.Common.Custom.BooleanField(
name = "boolean",
value = true,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked username",
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
),
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
name = "linked password",
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
),
),
requiresReprompt = true,
)
}
fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemType.Login =
VaultItemState.ViewState.Content.ItemType.Login(
passwordHistoryCount = 1.takeUnless { isEmpty },
username = "username".takeUnless { isEmpty },
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
password = "password",
isVisible = false,
)
.takeUnless { isEmpty },
uris = if (isEmpty) {
emptyList()
} else {
listOf(
VaultItemState.ViewState.Content.ItemType.Login.UriData(
uri = "www.example.com",
isCopyable = true,
isLaunchable = true,
),
)
},
passwordRevisionDate = "1/1/70 12:16 AM".takeUnless { isEmpty },
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
.takeUnless { isEmpty },
)
fun createIdentityContent(
isEmpty: Boolean,
address: String = DEFAULT_ADDRESS,
identityName: String = DEFAULT_IDENTITY_NAME,
): VaultItemState.ViewState.Content.ItemType.Identity =
VaultItemState.ViewState.Content.ItemType.Identity(
username = "username".takeUnless { isEmpty },
identityName = identityName.takeUnless { isEmpty },
company = "company".takeUnless { isEmpty },
ssn = "ssn".takeUnless { isEmpty },
passportNumber = "passportNumber".takeUnless { isEmpty },
licenseNumber = "licenseNumber".takeUnless { isEmpty },
email = "email".takeUnless { isEmpty },
phone = "phone".takeUnless { isEmpty },
address = address.takeUnless { isEmpty },
)