mirror of
https://github.com/bitwarden/android.git
synced 2024-11-29 14:28:55 +03:00
BIT-514: View identity item UI (#461)
This commit is contained in:
parent
cd1f703ba7
commit
0c05855e6b
16 changed files with 1066 additions and 369 deletions
|
@ -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
|
||||
}
|
|
@ -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() }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -86,4 +86,16 @@ class StringExtensionsTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capitalize should return a capitalized string`() {
|
||||
val initialString = "lowercase"
|
||||
|
||||
val result = initialString.capitalize()
|
||||
|
||||
assertEquals(
|
||||
"Lowercase",
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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 },
|
||||
)
|
Loading…
Reference in a new issue