mirror of
https://github.com/bitwarden/android.git
synced 2026-03-22 12:32:53 -05:00
[PM-18067] Consolidate item name fields into ItemHeader (#4766)
This commit is contained in:
@@ -108,7 +108,7 @@ fun BitwardenTextField(
|
||||
label: String?,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
cardStyle: CardStyle?,
|
||||
modifier: Modifier = Modifier,
|
||||
tooltip: TooltipData? = null,
|
||||
placeholder: String? = null,
|
||||
@@ -207,7 +207,7 @@ fun BitwardenTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
supportingContent: (@Composable ColumnScope.() -> Unit)?,
|
||||
cardStyle: CardStyle,
|
||||
cardStyle: CardStyle?,
|
||||
modifier: Modifier = Modifier,
|
||||
tooltip: TooltipData? = null,
|
||||
supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
|
||||
@@ -378,7 +378,7 @@ fun BitwardenTextField(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
?: Spacer(modifier = Modifier.height(height = 6.dp))
|
||||
?: Spacer(modifier = Modifier.height(height = cardStyle?.let { 6.dp } ?: 0.dp))
|
||||
}
|
||||
val filteredAutoCompleteList = autoCompleteOptions
|
||||
.filter { option ->
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.x8bit.bitwarden.ui.platform.components.header
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.AnimationConstants
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -12,6 +16,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -21,17 +26,25 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Reusable header element that is clickable for expanding or collapsing content.
|
||||
*
|
||||
* @param collapsedText Text to display when the content is collapsed.
|
||||
* @param expandedText Text to display when the content is expanded.
|
||||
* @param showExpansionIndicator Whether to show an indicator to expand or collapse the content.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenExpandingHeader(
|
||||
isExpanded: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = stringResource(id = R.string.additional_options),
|
||||
collapsedText: String = stringResource(id = R.string.additional_options),
|
||||
expandedText: String = collapsedText,
|
||||
showExpansionIndicator: Boolean = true,
|
||||
shape: Shape = BitwardenTheme.shapes.content,
|
||||
insets: PaddingValues = PaddingValues(top = 16.dp, bottom = 8.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(shape = BitwardenTheme.shapes.content)
|
||||
.clip(shape = shape)
|
||||
.clickable(
|
||||
onClickLabel = stringResource(
|
||||
id = if (isExpanded) R.string.options_expanded else R.string.options_collapsed,
|
||||
@@ -39,26 +52,51 @@ fun BitwardenExpandingHeader(
|
||||
onClick = onClick,
|
||||
)
|
||||
.minimumInteractiveComponentSize()
|
||||
.padding(top = 16.dp, bottom = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(paddingValues = insets)
|
||||
.semantics(mergeDescendants = true) {},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = BitwardenTheme.colorScheme.text.interaction,
|
||||
style = BitwardenTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
val iconRotationDegrees = animateFloatAsState(
|
||||
targetValue = if (isExpanded) 0f else 180f,
|
||||
label = "expanderIconRotationAnimation",
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_chevron_up_small),
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.icon.secondary,
|
||||
modifier = Modifier.rotate(degrees = iconRotationDegrees.value),
|
||||
)
|
||||
Crossfade(
|
||||
targetState = isExpanded,
|
||||
label = "BitwardenExpandingHeaderTitle_animation",
|
||||
// Make the animation shorter when the text is the same to avoid crossfading the same
|
||||
// text.
|
||||
animationSpec = tween(
|
||||
durationMillis = if (expandedText != collapsedText) {
|
||||
AnimationConstants.DefaultDurationMillis
|
||||
} else {
|
||||
0
|
||||
},
|
||||
),
|
||||
) { expanded ->
|
||||
if (expanded) {
|
||||
Text(
|
||||
text = expandedText,
|
||||
color = BitwardenTheme.colorScheme.text.interaction,
|
||||
style = BitwardenTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = collapsedText,
|
||||
color = BitwardenTheme.colorScheme.text.interaction,
|
||||
style = BitwardenTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showExpansionIndicator) {
|
||||
val iconRotationDegrees = animateFloatAsState(
|
||||
targetValue = if (isExpanded) 0f else 180f,
|
||||
label = "expanderIconRotationAnimation",
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_chevron_up_small),
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.icon.secondary,
|
||||
modifier = Modifier.rotate(degrees = iconRotationDegrees.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ data class BitwardenColorScheme(
|
||||
val toggleButton: ToggleButtonColors,
|
||||
val sliderButton: SliderButtonColors,
|
||||
val status: StatusColors,
|
||||
val illustration: IllustrationColors,
|
||||
) {
|
||||
/**
|
||||
* Defines all the text colors for the app.
|
||||
@@ -124,4 +125,13 @@ data class BitwardenColorScheme(
|
||||
val weak2: Color,
|
||||
val error: Color,
|
||||
)
|
||||
|
||||
/**
|
||||
* Defines all the illustration colors for the app.
|
||||
*/
|
||||
@Immutable
|
||||
data class IllustrationColors(
|
||||
val outline: Color,
|
||||
val backgroundPrimary: Color,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@ val darkBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme(
|
||||
weak2 = PrimitiveColors.yellow200,
|
||||
error = PrimitiveColors.red200,
|
||||
),
|
||||
illustration = BitwardenColorScheme.IllustrationColors(
|
||||
outline = PrimitiveColors.blue500,
|
||||
backgroundPrimary = PrimitiveColors.blue200,
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -137,6 +141,10 @@ val lightBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme(
|
||||
weak2 = PrimitiveColors.yellow300,
|
||||
error = PrimitiveColors.red300,
|
||||
),
|
||||
illustration = BitwardenColorScheme.IllustrationColors(
|
||||
outline = PrimitiveColors.blue700,
|
||||
backgroundPrimary = PrimitiveColors.blue100,
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -211,6 +219,10 @@ fun dynamicBitwardenColorScheme(
|
||||
weak2 = defaultTheme.status.weak2,
|
||||
error = defaultTheme.status.error,
|
||||
),
|
||||
illustration = BitwardenColorScheme.IllustrationColors(
|
||||
outline = materialColorScheme.tertiaryContainer,
|
||||
backgroundPrimary = materialColorScheme.onTertiaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@ data class BitwardenShapes(
|
||||
val progressIndicator: CornerBasedShape,
|
||||
val segmentedControl: CornerBasedShape,
|
||||
val snackbar: CornerBasedShape,
|
||||
val favicon: CornerBasedShape,
|
||||
)
|
||||
|
||||
@@ -22,4 +22,5 @@ val bitwardenShapes: BitwardenShapes = BitwardenShapes(
|
||||
progressIndicator = CircleShape,
|
||||
segmentedControl = CircleShape,
|
||||
snackbar = RoundedCornerShape(size = 8.dp),
|
||||
favicon = CircleShape,
|
||||
)
|
||||
|
||||
@@ -9,6 +9,10 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -22,8 +26,8 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.ItemNameField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
@@ -41,21 +45,26 @@ fun VaultItemCardContent(
|
||||
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
LazyColumn(modifier = modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
itemHeader(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
iconData = commonState.iconData,
|
||||
relatedLocations = commonState.relatedLocations,
|
||||
iconTestTag = "CardItemNameIcon",
|
||||
textFieldTestTag = "CardItemNameEntry",
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
ItemNameField(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
textFieldTestTag = "CardItemNameEntry",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
cardState.cardholderName?.let { cardholderName ->
|
||||
item {
|
||||
item(key = "cardholderName") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.cardholder_name),
|
||||
value = cardholderName,
|
||||
@@ -71,12 +80,13 @@ fun VaultItemCardContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
cardState.number?.let { numberData ->
|
||||
item {
|
||||
item(key = "cardNumber") {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.number),
|
||||
value = numberData.number,
|
||||
@@ -103,13 +113,14 @@ fun VaultItemCardContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) {
|
||||
item {
|
||||
item(key = "cardBrand") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.brand),
|
||||
value = cardState.brand.shortName(),
|
||||
@@ -125,13 +136,14 @@ fun VaultItemCardContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cardState.expiration?.let { expiration ->
|
||||
item {
|
||||
item(key = "expiration") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.expiration),
|
||||
value = expiration,
|
||||
@@ -147,13 +159,14 @@ fun VaultItemCardContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cardState.securityCode?.let { securityCodeData ->
|
||||
item {
|
||||
item(key = "securityCode") {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.security_code),
|
||||
value = securityCodeData.code,
|
||||
@@ -165,7 +178,9 @@ fun VaultItemCardContent(
|
||||
actions = {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = R.drawable.ic_copy,
|
||||
contentDescription = stringResource(id = R.string.copy_security_code),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.copy_security_code,
|
||||
),
|
||||
onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick,
|
||||
modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"),
|
||||
)
|
||||
@@ -180,20 +195,22 @@ fun VaultItemCardContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.notes?.let { notes ->
|
||||
item {
|
||||
item(key = "notes") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.additional_options),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
@@ -214,23 +231,28 @@ fun VaultItemCardContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
item {
|
||||
item(key = "customFieldsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.custom_fields),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
items(customFields) { customField ->
|
||||
itemsIndexed(
|
||||
items = customFields,
|
||||
key = { index, _ -> "customField_$index" },
|
||||
) { _, customField ->
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
CustomField(
|
||||
customField = customField,
|
||||
@@ -240,29 +262,35 @@ fun VaultItemCardContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
|
||||
item {
|
||||
item(key = "attachmentsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.attachments),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
itemsIndexed(attachments) { index, attachmentItem ->
|
||||
itemsIndexed(
|
||||
items = attachments,
|
||||
key = { index, _ -> "attachment_$index" },
|
||||
) { index, attachmentItem ->
|
||||
AttachmentItemContent(
|
||||
modifier = Modifier
|
||||
.testTag("CipherAttachment")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
attachmentItem = attachmentItem,
|
||||
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
|
||||
.onAttachmentDownloadClick,
|
||||
@@ -271,7 +299,7 @@ fun VaultItemCardContent(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
item(key = "lastUpdated") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
VaultItemUpdateText(
|
||||
header = "${stringResource(id = R.string.date_updated)}: ",
|
||||
@@ -279,7 +307,8 @@ fun VaultItemCardContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 12.dp),
|
||||
.padding(horizontal = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
||||
@@ -9,6 +9,10 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -21,8 +25,8 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.ItemNameField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
|
||||
|
||||
@@ -38,21 +42,28 @@ fun VaultItemIdentityContent(
|
||||
vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
item {
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
itemHeader(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
iconData = commonState.iconData,
|
||||
relatedLocations = commonState.relatedLocations,
|
||||
iconTestTag = "IdentityItemNameIcon",
|
||||
textFieldTestTag = "IdentityItemNameEntry",
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
ItemNameField(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
textFieldTestTag = "ItemNameEntry",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
identityState.identityName?.let { identityName ->
|
||||
item {
|
||||
item(key = "identityName") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.identity_name),
|
||||
value = identityName,
|
||||
@@ -68,12 +79,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.username?.let { username ->
|
||||
item {
|
||||
item(key = "username") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.username),
|
||||
value = username,
|
||||
@@ -89,12 +101,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.company?.let { company ->
|
||||
item {
|
||||
item(key = "company") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.company),
|
||||
value = company,
|
||||
@@ -110,12 +123,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.ssn?.let { ssn ->
|
||||
item {
|
||||
item(key = "ssn") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.ssn),
|
||||
value = ssn,
|
||||
@@ -131,12 +145,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.passportNumber?.let { passportNumber ->
|
||||
item {
|
||||
item(key = "passportNumber") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.passport_number),
|
||||
value = passportNumber,
|
||||
@@ -152,12 +167,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.licenseNumber?.let { licenseNumber ->
|
||||
item {
|
||||
item(key = "licenseNumber") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.license_number),
|
||||
value = licenseNumber,
|
||||
@@ -173,12 +189,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.email?.let { email ->
|
||||
item {
|
||||
item(key = "email") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.email),
|
||||
value = email,
|
||||
@@ -194,12 +211,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.phone?.let { phone ->
|
||||
item {
|
||||
item(key = "phone") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.phone),
|
||||
value = phone,
|
||||
@@ -215,12 +233,13 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
identityState.address?.let { address ->
|
||||
item {
|
||||
item(key = "address") {
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = R.string.address),
|
||||
value = address,
|
||||
@@ -236,19 +255,21 @@ fun VaultItemIdentityContent(
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
commonState.notes?.let { notes ->
|
||||
item {
|
||||
item(key = "notes") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.additional_options),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
IdentityCopyField(
|
||||
@@ -261,23 +282,28 @@ fun VaultItemIdentityContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
item {
|
||||
item(key = "customFieldsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.custom_fields),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
items(customFields) { customField ->
|
||||
items(
|
||||
items = customFields,
|
||||
key = { "customField_$it" },
|
||||
) { customField ->
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
CustomField(
|
||||
customField = customField,
|
||||
@@ -287,29 +313,35 @@ fun VaultItemIdentityContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
|
||||
item {
|
||||
item(key = "attachmentsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.attachments),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
itemsIndexed(attachments) { index, attachmentItem ->
|
||||
itemsIndexed(
|
||||
items = attachments,
|
||||
key = { index, _ -> "attachment_$index" },
|
||||
) { index, attachmentItem ->
|
||||
AttachmentItemContent(
|
||||
modifier = Modifier
|
||||
.testTag("CipherAttachment")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
attachmentItem = attachmentItem,
|
||||
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
|
||||
.onAttachmentDownloadClick,
|
||||
@@ -318,7 +350,7 @@ fun VaultItemIdentityContent(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
item(key = "lastUpdated") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
VaultItemUpdateText(
|
||||
header = "${stringResource(id = R.string.date_updated)}: ",
|
||||
@@ -326,7 +358,8 @@ fun VaultItemIdentityContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 12.dp),
|
||||
.padding(horizontal = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
||||
@@ -8,9 +8,12 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -30,8 +33,8 @@ import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.ItemNameField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
@@ -50,47 +53,40 @@ fun VaultItemLoginContent(
|
||||
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.item_details),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
item {
|
||||
ItemNameField(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
textFieldTestTag = "LoginItemNameEntry",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
itemHeader(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
iconData = commonState.iconData,
|
||||
relatedLocations = commonState.relatedLocations,
|
||||
iconTestTag = "LoginItemNameIcon",
|
||||
textFieldTestTag = "LoginItemNameEntry",
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
if (loginItemState.hasLoginCredentials) {
|
||||
item {
|
||||
item(key = "loginCredentialsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.login_credentials),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
loginItemState.username?.let { username ->
|
||||
item {
|
||||
item(key = "username") {
|
||||
UsernameField(
|
||||
username = username,
|
||||
onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick,
|
||||
@@ -99,14 +95,15 @@ fun VaultItemLoginContent(
|
||||
?.let { CardStyle.Top(dividerPadding = 0.dp) }
|
||||
?: CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loginItemState.passwordData?.let { passwordData ->
|
||||
item {
|
||||
item(key = "passwordData") {
|
||||
PasswordField(
|
||||
passwordData = passwordData,
|
||||
onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick,
|
||||
@@ -117,26 +114,28 @@ fun VaultItemLoginContent(
|
||||
?.let { CardStyle.Bottom }
|
||||
?: CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loginItemState.fido2CredentialCreationDateText?.let { creationDate ->
|
||||
item {
|
||||
item(key = "creationDate") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Fido2CredentialField(
|
||||
creationDate = creationDate(),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loginItemState.totpCodeItemData?.let { totpCodeItemData ->
|
||||
item {
|
||||
item(key = "totpCode") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TotpField(
|
||||
totpCodeItemData = totpCodeItemData,
|
||||
@@ -145,71 +144,83 @@ fun VaultItemLoginContent(
|
||||
onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers
|
||||
.onAuthenticatorHelpToolTipClick,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris ->
|
||||
item {
|
||||
item(key = "urisHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.autofill_options),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
|
||||
itemsIndexed(uris) { index, uriData ->
|
||||
itemsIndexed(
|
||||
items = uris,
|
||||
key = { index, _ -> "uri_$index" },
|
||||
) { index, uriData ->
|
||||
UriField(
|
||||
uriData = uriData,
|
||||
onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick,
|
||||
onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick,
|
||||
cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.notes?.let { notes ->
|
||||
item {
|
||||
item(key = "notes") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.additional_options),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
NotesField(
|
||||
notes = notes,
|
||||
onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
item {
|
||||
item(key = "customFieldsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.custom_fields),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
items(customFields) { customField ->
|
||||
itemsIndexed(
|
||||
items = customFields,
|
||||
key = { index, _ -> "customField_$index" },
|
||||
) { _, customField ->
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
CustomField(
|
||||
customField = customField,
|
||||
@@ -218,29 +229,35 @@ fun VaultItemLoginContent(
|
||||
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
|
||||
item {
|
||||
item(key = "attachmentsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.attachments),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
itemsIndexed(attachments) { index, attachmentItem ->
|
||||
itemsIndexed(
|
||||
items = attachments,
|
||||
key = { index, _ -> "attachment_$index" },
|
||||
) { index, attachmentItem ->
|
||||
AttachmentItemContent(
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.animateItem(),
|
||||
attachmentItem = attachmentItem,
|
||||
cardStyle = attachments.toListItemCardStyle(index = index),
|
||||
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
|
||||
@@ -249,20 +266,21 @@ fun VaultItemLoginContent(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
item(key = "lastUpdated") {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
VaultItemUpdateText(
|
||||
header = "${stringResource(id = R.string.date_updated)}: ",
|
||||
text = commonState.lastUpdated,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 12.dp),
|
||||
.padding(horizontal = 12.dp)
|
||||
.fillMaxWidth()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
loginItemState.passwordRevisionDate?.let { revisionDate ->
|
||||
item {
|
||||
item(key = "revisionDate") {
|
||||
Spacer(modifier = Modifier.height(height = 4.dp))
|
||||
VaultItemUpdateText(
|
||||
header = "${stringResource(id = R.string.date_password_updated)}: ",
|
||||
@@ -270,13 +288,14 @@ fun VaultItemLoginContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 12.dp),
|
||||
.padding(horizontal = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loginItemState.passwordHistoryCount?.let { passwordHistoryCount ->
|
||||
item {
|
||||
item(key = "passwordHistoryCount") {
|
||||
Spacer(modifier = Modifier.height(height = 4.dp))
|
||||
BitwardenHyperTextLink(
|
||||
annotatedResId = R.string.password_history_count,
|
||||
@@ -288,7 +307,8 @@ fun VaultItemLoginContent(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 12.dp),
|
||||
.padding(horizontal = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ 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.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -25,7 +28,7 @@ import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.ItemNameField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
|
||||
/**
|
||||
@@ -38,21 +41,26 @@ fun VaultItemSecureNoteContent(
|
||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
LazyColumn(modifier = modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
ItemNameField(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
textFieldTestTag = "ItemNameEntry",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
itemHeader(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
iconData = commonState.iconData,
|
||||
relatedLocations = commonState.relatedLocations,
|
||||
iconTestTag = "SecureNoteItemNameIcon",
|
||||
textFieldTestTag = "SecureNoteItemNameEntry",
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
|
||||
commonState.notes?.let { notes ->
|
||||
item {
|
||||
item(key = "notes") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.notes),
|
||||
@@ -72,24 +80,29 @@ fun VaultItemSecureNoteContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
item {
|
||||
item(key = "customFieldsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.custom_fields),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
items(customFields) { customField ->
|
||||
itemsIndexed(
|
||||
items = customFields,
|
||||
key = { index, _ -> "customField_$index" },
|
||||
) { _, customField ->
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
CustomField(
|
||||
customField = customField,
|
||||
@@ -99,29 +112,35 @@ fun VaultItemSecureNoteContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
|
||||
item {
|
||||
item(key = "attachmentsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.attachments),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
itemsIndexed(attachments) { index, attachmentItem ->
|
||||
itemsIndexed(
|
||||
items = attachments,
|
||||
key = { index, _ -> "attachment_$index" },
|
||||
) { index, attachmentItem ->
|
||||
AttachmentItemContent(
|
||||
modifier = Modifier
|
||||
.testTag("CipherAttachment")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
attachmentItem = attachmentItem,
|
||||
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
|
||||
.onAttachmentDownloadClick,
|
||||
@@ -130,14 +149,15 @@ fun VaultItemSecureNoteContent(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
item(key = "lastUpdated") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 12.dp)
|
||||
.semantics(mergeDescendants = true) { },
|
||||
.semantics(mergeDescendants = true) { }
|
||||
.animateItem(),
|
||||
) {
|
||||
Text(
|
||||
text = "${stringResource(id = R.string.date_updated)}: ",
|
||||
|
||||
@@ -6,9 +6,12 @@ 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.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -22,8 +25,8 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.ItemNameField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
||||
|
||||
@@ -39,31 +42,22 @@ fun VaultItemSshKeyContent(
|
||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
LazyColumn(modifier = modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.item_information),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
ItemNameField(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
textFieldTestTag = "SshKeyItemNameEntry",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
itemHeader(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
iconData = commonState.iconData,
|
||||
relatedLocations = commonState.relatedLocations,
|
||||
iconTestTag = "SshKeyItemNameIcon",
|
||||
textFieldTestTag = "SshKeyItemNameEntry",
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
item(key = "publicKey") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.public_key),
|
||||
@@ -83,11 +77,12 @@ fun VaultItemSshKeyContent(
|
||||
modifier = Modifier
|
||||
.testTag("SshKeyItemPublicKeyEntry")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
item(key = "privateKey") {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.private_key),
|
||||
value = sshKeyItemState.privateKey,
|
||||
@@ -109,11 +104,12 @@ fun VaultItemSshKeyContent(
|
||||
modifier = Modifier
|
||||
.testTag("SshKeyItemPrivateKeyEntry")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
item(key = "fingerprint") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.fingerprint),
|
||||
value = sshKeyItemState.fingerprint,
|
||||
@@ -132,19 +128,21 @@ fun VaultItemSshKeyContent(
|
||||
modifier = Modifier
|
||||
.testTag("SshKeyItemFingerprintEntry")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
commonState.notes?.let { notes ->
|
||||
item {
|
||||
item(key = "notes") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.additional_options),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
@@ -165,23 +163,28 @@ fun VaultItemSshKeyContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
item {
|
||||
item(key = "customFieldsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.custom_fields),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
items(customFields) { customField ->
|
||||
itemsIndexed(
|
||||
items = customFields,
|
||||
key = { index, _ -> "customField_$index" },
|
||||
) { _, customField ->
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
CustomField(
|
||||
customField = customField,
|
||||
@@ -191,28 +194,34 @@ fun VaultItemSshKeyContent(
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
|
||||
item {
|
||||
item(key = "attachmentsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.attachments),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
itemsIndexed(attachments) { index, attachmentItem ->
|
||||
itemsIndexed(
|
||||
items = attachments,
|
||||
key = { index, _ -> "attachment_$index" },
|
||||
) { index, attachmentItem ->
|
||||
AttachmentItemContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
attachmentItem = attachmentItem,
|
||||
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
|
||||
.onAttachmentDownloadClick,
|
||||
@@ -221,7 +230,7 @@ fun VaultItemSshKeyContent(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
item(key = "lastUpdated") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
VaultItemUpdateText(
|
||||
header = "${stringResource(id = R.string.date_updated)}: ",
|
||||
@@ -230,6 +239,7 @@ fun VaultItemSshKeyContent(
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 12.dp)
|
||||
.animateItem()
|
||||
.testTag("SshKeyItemLastUpdated"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.mapNullable
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
@@ -25,8 +28,10 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections
|
||||
@@ -35,8 +40,11 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,7 +59,7 @@ private const val KEY_TEMP_ATTACHMENT = "tempAttachmentFile"
|
||||
/**
|
||||
* ViewModel responsible for handling user interactions in the vault item screen
|
||||
*/
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
|
||||
@HiltViewModel
|
||||
class VaultItemViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
@@ -60,6 +68,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val fileManager: FileManager,
|
||||
private val organizationEventManager: OrganizationEventManager,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) : BaseViewModel<VaultItemState, VaultItemEvent, VaultItemAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
@@ -69,6 +79,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
cipherType = args.cipherType,
|
||||
viewState = VaultItemState.ViewState.Loading,
|
||||
dialog = null,
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
)
|
||||
},
|
||||
) {
|
||||
@@ -91,7 +103,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
authRepository.userStateFlow,
|
||||
vaultRepository.getAuthCodeFlow(state.vaultItemId),
|
||||
vaultRepository.collectionsStateFlow,
|
||||
) { cipherViewState, userState, authCodeState, collectionsState ->
|
||||
vaultRepository.foldersStateFlow,
|
||||
) { cipherViewState, userState, authCodeState, collectionsState, folderState ->
|
||||
val totpCodeData = authCodeState.data?.let {
|
||||
TotpCodeItemData(
|
||||
periodSeconds = it.periodSeconds,
|
||||
@@ -106,35 +119,68 @@ class VaultItemViewModel @Inject constructor(
|
||||
cipherViewState,
|
||||
authCodeState,
|
||||
collectionsState,
|
||||
) { _, _, _ ->
|
||||
folderState,
|
||||
) { _, _, _, _ ->
|
||||
// We are only combining the DataStates to know the overall state,
|
||||
// we map it to the appropriate value below.
|
||||
}
|
||||
.mapNullable {
|
||||
val canDelete = collectionsState
|
||||
.data
|
||||
val cipherView = cipherViewState.data
|
||||
val canDelete = collectionsState.data
|
||||
.hasDeletePermissionInAtLeastOneCollection(
|
||||
collectionIds = cipherViewState.data?.collectionIds,
|
||||
collectionIds = cipherView?.collectionIds,
|
||||
)
|
||||
|
||||
val canAssignToCollections = collectionsState
|
||||
.data
|
||||
.canAssignToCollections(cipherViewState.data?.collectionIds)
|
||||
val canAssignToCollections = collectionsState.data
|
||||
.canAssignToCollections(cipherView?.collectionIds)
|
||||
|
||||
val canEdit = cipherViewState.data?.edit == true
|
||||
val canEdit = cipherView?.edit == true
|
||||
val organizationName = cipherView
|
||||
?.organizationId
|
||||
?.let { orgId ->
|
||||
userState
|
||||
?.activeAccount
|
||||
?.organizations
|
||||
?.firstOrNull { it.id == orgId }
|
||||
?.name
|
||||
}
|
||||
val cipherCollections = cipherView
|
||||
?.collectionIds
|
||||
.orEmpty()
|
||||
val collections = collectionsState.data
|
||||
?.filter { cipherCollections.contains(it.id) }
|
||||
?.map { it.name }
|
||||
.orEmpty()
|
||||
val folderName = cipherView
|
||||
?.folderId
|
||||
?.let { folderId ->
|
||||
folderState.data?.firstOrNull { folder -> folderId == folder.id }
|
||||
}
|
||||
?.name
|
||||
val relatedLocations = persistentListOfNotNull(
|
||||
organizationName?.let { VaultItemLocation.Organization(it) },
|
||||
*collections.map { VaultItemLocation.Collection(it) }.toTypedArray(),
|
||||
folderName?.let { VaultItemLocation.Folder(it) },
|
||||
)
|
||||
|
||||
VaultItemStateData(
|
||||
cipher = cipherViewState.data,
|
||||
cipher = cipherView,
|
||||
totpCodeItemData = totpCodeData,
|
||||
canDelete = canDelete,
|
||||
canAssociateToCollections = canAssignToCollections,
|
||||
canEdit = canEdit,
|
||||
relatedLocations = relatedLocations,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository.isIconLoadingDisabledFlow
|
||||
.map { VaultItemAction.Internal.IsIconLoadingDisabledUpdateReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: VaultItemAction) {
|
||||
@@ -1046,6 +1092,10 @@ class VaultItemViewModel @Inject constructor(
|
||||
is VaultItemAction.Internal.AttachmentFinishedSavingToDisk -> {
|
||||
handleAttachmentFinishedSavingToDisk(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.Internal.IsIconLoadingDisabledUpdateReceive -> {
|
||||
handleIsIconLoadingDisabledUpdateReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,6 +1197,9 @@ class VaultItemViewModel @Inject constructor(
|
||||
canDelete = this.data?.canDelete == true,
|
||||
canAssignToCollections = this.data?.canAssociateToCollections == true,
|
||||
canEdit = this.data?.canEdit == true,
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
relatedLocations = this.data?.relatedLocations.orEmpty().toImmutableList(),
|
||||
)
|
||||
?: VaultItemState.ViewState.Error(message = errorText)
|
||||
|
||||
@@ -1285,6 +1338,12 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIsIconLoadingDisabledUpdateReceive(
|
||||
action: VaultItemAction.Internal.IsIconLoadingDisabledUpdateReceive,
|
||||
) {
|
||||
mutableStateFlow.update { it.copy(isIconLoadingDisabled = action.isDisabled) }
|
||||
}
|
||||
|
||||
//endregion Internal Type Handlers
|
||||
|
||||
private fun updateDialogState(dialog: VaultItemState.DialogState?) {
|
||||
@@ -1373,6 +1432,8 @@ data class VaultItemState(
|
||||
val cipherType: VaultItemCipherType,
|
||||
val viewState: ViewState,
|
||||
val dialog: DialogState?,
|
||||
val baseIconUrl: String,
|
||||
val isIconLoadingDisabled: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
@@ -1487,7 +1548,7 @@ data class VaultItemState(
|
||||
* @property canDelete Indicates if the cipher can be deleted.
|
||||
* @property canAssignToCollections Indicates if the cipher can be assigned to
|
||||
* collections.
|
||||
* @property favorite Indicates that the cipher is favoried.
|
||||
* @property favorite Indicates that the cipher is marked as a favorite item.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Common(
|
||||
@@ -1504,6 +1565,8 @@ data class VaultItemState(
|
||||
val canAssignToCollections: Boolean,
|
||||
val canEdit: Boolean,
|
||||
val favorite: Boolean,
|
||||
val iconData: IconData,
|
||||
val relatedLocations: ImmutableList<VaultItemLocation>,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
@@ -2248,6 +2311,13 @@ sealed class VaultItemAction {
|
||||
val isSaved: Boolean,
|
||||
val file: File,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the `isIconLoadingDisabled` setting has changed.
|
||||
*/
|
||||
data class IsIconLoadingDisabledUpdateReceive(
|
||||
val isDisabled: Boolean,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.vector.VectorPainter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenExpandingHeader
|
||||
import com.x8bit.bitwarden.ui.platform.components.icon.BitwardenIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* The max number of items that can be displayed before the "show more" text is visible.
|
||||
*/
|
||||
private const val EXPANDABLE_THRESHOLD = 2
|
||||
|
||||
/**
|
||||
* Reusable composable for displaying the cipher name, favorite status, and related locations.
|
||||
*/
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
fun LazyListScope.itemHeader(
|
||||
value: String,
|
||||
isFavorite: Boolean,
|
||||
relatedLocations: ImmutableList<VaultItemLocation>,
|
||||
iconData: IconData,
|
||||
isExpanded: Boolean,
|
||||
iconTestTag: String? = null,
|
||||
textFieldTestTag: String? = null,
|
||||
onExpandClick: () -> Unit,
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.defaultMinSize(minHeight = 60.dp)
|
||||
.cardStyle(
|
||||
cardStyle = CardStyle.Top(),
|
||||
paddingVertical = 0.dp,
|
||||
)
|
||||
.padding(start = 16.dp),
|
||||
) {
|
||||
ItemHeaderIcon(
|
||||
iconData = iconData,
|
||||
testTag = iconTestTag,
|
||||
modifier = Modifier.size(36.dp),
|
||||
)
|
||||
BitwardenTextField(
|
||||
label = null,
|
||||
value = value,
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
actions = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = if (isFavorite) {
|
||||
R.drawable.ic_favorite_full
|
||||
} else {
|
||||
R.drawable.ic_favorite_empty
|
||||
},
|
||||
),
|
||||
contentDescription = stringResource(
|
||||
id = if (isFavorite) R.string.favorite else R.string.unfavorite,
|
||||
),
|
||||
modifier = Modifier.padding(all = 12.dp),
|
||||
)
|
||||
},
|
||||
textFieldTestTag = textFieldTestTag,
|
||||
cardStyle = null,
|
||||
textStyle = BitwardenTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (relatedLocations.isEmpty()) {
|
||||
item(key = "noFolder") {
|
||||
ItemLocationListItem(
|
||||
vectorPainter = rememberVectorPainter(R.drawable.ic_folder),
|
||||
text = stringResource(R.string.no_folder),
|
||||
iconTestTag = "NoFolderIcon",
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.cardStyle(
|
||||
cardStyle = CardStyle.Bottom,
|
||||
paddingVertical = 0.dp,
|
||||
paddingHorizontal = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
key = { index, _ -> "locations_$index" },
|
||||
items = relatedLocations.take(EXPANDABLE_THRESHOLD),
|
||||
) { index, location ->
|
||||
ItemLocationListItem(
|
||||
vectorPainter = rememberVectorPainter(location.icon),
|
||||
iconTestTag = "ItemLocationIcon",
|
||||
text = location.name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem()
|
||||
.cardStyle(
|
||||
cardStyle = if (index == relatedLocations.size - 1) {
|
||||
CardStyle.Bottom
|
||||
} else {
|
||||
CardStyle.Middle(hasDivider = false)
|
||||
},
|
||||
paddingVertical = 0.dp,
|
||||
paddingHorizontal = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
items(
|
||||
key = { "expandableLocations_$it" },
|
||||
items = relatedLocations.drop(EXPANDABLE_THRESHOLD),
|
||||
) {
|
||||
ItemLocationListItem(
|
||||
vectorPainter = rememberVectorPainter(it.icon),
|
||||
text = it.name,
|
||||
iconTestTag = "ItemLocationIcon",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem()
|
||||
.cardStyle(
|
||||
cardStyle = CardStyle.Middle(hasDivider = false),
|
||||
paddingVertical = 0.dp,
|
||||
paddingHorizontal = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (relatedLocations.size > EXPANDABLE_THRESHOLD) {
|
||||
item(key = "expandableLocationsShowMore") {
|
||||
BitwardenExpandingHeader(
|
||||
collapsedText = stringResource(R.string.show_more),
|
||||
expandedText = stringResource(R.string.show_less),
|
||||
isExpanded = isExpanded,
|
||||
onClick = onExpandClick,
|
||||
showExpansionIndicator = false,
|
||||
shape = RectangleShape,
|
||||
insets = PaddingValues(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem()
|
||||
.cardStyle(
|
||||
cardStyle = CardStyle.Bottom,
|
||||
paddingVertical = 0.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemHeaderIcon(
|
||||
iconData: IconData,
|
||||
modifier: Modifier = Modifier,
|
||||
testTag: String? = null,
|
||||
) {
|
||||
val isLocalIcon = iconData is IconData.Local
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.then(
|
||||
if (isLocalIcon) {
|
||||
Modifier.background(
|
||||
color = BitwardenTheme.colorScheme.illustration.backgroundPrimary,
|
||||
shape = BitwardenTheme.shapes.favicon,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
BitwardenIcon(
|
||||
iconData = iconData,
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.illustration.outline,
|
||||
modifier = Modifier
|
||||
.nullableTestTag(testTag)
|
||||
.then(
|
||||
if (!isLocalIcon) Modifier.fillMaxSize() else Modifier,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyItemScope.ItemLocationListItem(
|
||||
vectorPainter: VectorPainter,
|
||||
iconTestTag: String?,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = vectorPainter,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.nullableTestTag(iconTestTag),
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.testTag("ItemLocationText"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Previews
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ItemHeader_LocalIcon_Preview() {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
BitwardenTheme {
|
||||
LazyColumn {
|
||||
itemHeader(
|
||||
value = "Login without favicon",
|
||||
isFavorite = true,
|
||||
iconData = IconData.Local(
|
||||
iconRes = R.drawable.ic_globe,
|
||||
),
|
||||
relatedLocations = persistentListOf(),
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ItemHeader_NetworkIcon_Preview() {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
BitwardenTheme {
|
||||
LazyColumn {
|
||||
itemHeader(
|
||||
value = "Login with favicon",
|
||||
isFavorite = true,
|
||||
iconData = IconData.Network(
|
||||
uri = "mockuri",
|
||||
fallbackIconRes = R.drawable.ic_globe,
|
||||
),
|
||||
relatedLocations = persistentListOf(),
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ItemHeader_Organization_Preview() {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
BitwardenTheme {
|
||||
LazyColumn {
|
||||
itemHeader(
|
||||
value = "Login without favicon",
|
||||
isFavorite = true,
|
||||
iconData = IconData.Local(
|
||||
iconRes = R.drawable.ic_globe,
|
||||
),
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Organization("Stark Industries"),
|
||||
),
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ItemNameField_Org_SingleCollection_Preview() {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
BitwardenTheme {
|
||||
LazyColumn {
|
||||
itemHeader(
|
||||
value = "Login without favicon",
|
||||
isFavorite = true,
|
||||
iconData = IconData.Local(
|
||||
iconRes = R.drawable.ic_globe,
|
||||
),
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Organization("Stark Industries"),
|
||||
VaultItemLocation.Collection("Marketing"),
|
||||
),
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ItemNameField_Org_MultiCollection_Preview() {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
BitwardenTheme {
|
||||
LazyColumn {
|
||||
itemHeader(
|
||||
value = "Login without favicon",
|
||||
isFavorite = true,
|
||||
iconData = IconData.Local(
|
||||
iconRes = R.drawable.ic_globe,
|
||||
),
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Organization("Stark Industries"),
|
||||
VaultItemLocation.Collection("Marketing"),
|
||||
VaultItemLocation.Collection("Product"),
|
||||
),
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ItemNameField_Org_SingleCollection_Folder_Preview() {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
BitwardenTheme {
|
||||
LazyColumn {
|
||||
itemHeader(
|
||||
value = "Note without favicon",
|
||||
isFavorite = true,
|
||||
iconData = IconData.Local(
|
||||
iconRes = R.drawable.ic_note,
|
||||
),
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Organization("Stark Industries"),
|
||||
VaultItemLocation.Collection("Marketing"),
|
||||
VaultItemLocation.Folder("Competition"),
|
||||
),
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion Previews
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.component
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
|
||||
/**
|
||||
* Reusable composable for displaying the cipher name and favorite status.
|
||||
*/
|
||||
@Composable
|
||||
fun ItemNameField(
|
||||
value: String,
|
||||
isFavorite: Boolean,
|
||||
textFieldTestTag: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.item_name_required),
|
||||
value = value,
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
actions = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = if (isFavorite) {
|
||||
R.drawable.ic_favorite_full
|
||||
} else {
|
||||
R.drawable.ic_favorite_empty
|
||||
},
|
||||
),
|
||||
contentDescription = stringResource(
|
||||
id = if (isFavorite) R.string.favorite else R.string.unfavorite,
|
||||
),
|
||||
modifier = Modifier.padding(all = 12.dp),
|
||||
)
|
||||
},
|
||||
textFieldTestTag = textFieldTestTag,
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.x8bit.bitwarden.R
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents the location of a vault item.
|
||||
*
|
||||
* A vault item can be located in an [Organization], [Collection], or a [Folder].
|
||||
* Each location type provides specific details and an associated icon.
|
||||
*/
|
||||
sealed class VaultItemLocation : Parcelable {
|
||||
/**
|
||||
* The name of the location. This can be the organization name, collection name, or folder name
|
||||
* depending on the location type.
|
||||
*/
|
||||
abstract val name: String
|
||||
|
||||
/**
|
||||
* Icon for the location
|
||||
*/
|
||||
@get:DrawableRes
|
||||
abstract val icon: Int
|
||||
|
||||
/**
|
||||
* Represents an organization assignment.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Organization(
|
||||
override val name: String,
|
||||
) : VaultItemLocation() {
|
||||
override val icon: Int
|
||||
get() = R.drawable.ic_organization
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection assignment.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Collection(
|
||||
override val name: String,
|
||||
) : VaultItemLocation() {
|
||||
override val icon: Int
|
||||
get() = R.drawable.ic_collections
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a folder assignment.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Folder(
|
||||
override val name: String,
|
||||
) : VaultItemLocation() {
|
||||
override val icon: Int
|
||||
get() = R.drawable.ic_folder
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.model
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* The state containing totp code item information and the cipher for the item.
|
||||
@@ -10,6 +11,7 @@ import com.bitwarden.vault.CipherView
|
||||
* @property canDelete Whether the item can be deleted.
|
||||
* @property canAssociateToCollections Whether the item can be associated to a collection.
|
||||
* @property canEdit Whether the item can be edited.
|
||||
* @property relatedLocations The locations the item is assigned to.
|
||||
*/
|
||||
data class VaultItemStateData(
|
||||
val cipher: CipherView?,
|
||||
@@ -17,4 +19,5 @@ data class VaultItemStateData(
|
||||
val canDelete: Boolean,
|
||||
val canAssociateToCollections: Boolean,
|
||||
val canEdit: Boolean,
|
||||
val relatedLocations: ImmutableList<VaultItemLocation>,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.util
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.vault.CardView
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
@@ -17,12 +18,16 @@ 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.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.time.Clock
|
||||
|
||||
private const val LAST_UPDATED_DATE_TIME_PATTERN: String = "M/d/yy hh:mm a"
|
||||
@@ -42,6 +47,9 @@ fun CipherView.toViewState(
|
||||
canDelete: Boolean,
|
||||
canAssignToCollections: Boolean,
|
||||
canEdit: Boolean,
|
||||
baseIconUrl: String,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
relatedLocations: ImmutableList<VaultItemLocation>,
|
||||
): VaultItemState.ViewState =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = VaultItemState.ViewState.Content.Common(
|
||||
@@ -86,6 +94,11 @@ fun CipherView.toViewState(
|
||||
canAssignToCollections = canAssignToCollections,
|
||||
canEdit = canEdit,
|
||||
favorite = this.favorite,
|
||||
iconData = this.toIconData(
|
||||
baseIconUrl = baseIconUrl,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
),
|
||||
relatedLocations = relatedLocations,
|
||||
),
|
||||
type = when (type) {
|
||||
CipherType.LOGIN -> {
|
||||
@@ -226,6 +239,35 @@ private fun Fido2Credential?.getCreationDateText(clock: Clock): Text? =
|
||||
)
|
||||
}
|
||||
|
||||
private fun CipherView.toIconData(
|
||||
baseIconUrl: String,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
): IconData {
|
||||
return when (this.type) {
|
||||
CipherType.LOGIN -> {
|
||||
login?.uris.toLoginIconData(
|
||||
baseIconUrl = baseIconUrl,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
usePasskeyDefaultIcon = false,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
IconData.Local(iconRes = this.type.iconRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@get:DrawableRes
|
||||
private val CipherType.iconRes: Int
|
||||
get() = when (this) {
|
||||
CipherType.SECURE_NOTE -> R.drawable.ic_note
|
||||
CipherType.CARD -> R.drawable.ic_payment_card
|
||||
CipherType.IDENTITY -> R.drawable.ic_id_card
|
||||
CipherType.SSH_KEY -> R.drawable.ic_ssh_key
|
||||
CipherType.LOGIN -> R.drawable.ic_globe
|
||||
}
|
||||
|
||||
private val IdentityView.identityAddress: String?
|
||||
get() = listOfNotNull(
|
||||
address1,
|
||||
|
||||
28
app/src/main/res/drawable/ic_organization.xml
Normal file
28
app/src/main/res/drawable/ic_organization.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M8.197,6.271C7.783,6.271 7.447,6.606 7.447,7.021C7.447,7.435 7.783,7.771 8.197,7.771H10.135C10.549,7.771 10.885,7.435 10.885,7.021C10.885,6.606 10.549,6.271 10.135,6.271H8.197Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M13.365,6.271C12.951,6.271 12.615,6.606 12.615,7.021C12.615,7.435 12.951,7.771 13.365,7.771H15.303C15.717,7.771 16.053,7.435 16.053,7.021C16.053,6.606 15.717,6.271 15.303,6.271H13.365Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M13.365,10.02C12.951,10.02 12.615,10.356 12.615,10.77C12.615,11.185 12.951,11.52 13.365,11.52H15.303C15.717,11.52 16.053,11.185 16.053,10.77C16.053,10.356 15.717,10.02 15.303,10.02H13.365Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M12.615,14.52C12.615,14.106 12.951,13.77 13.365,13.77H15.303C15.717,13.77 16.053,14.106 16.053,14.52C16.053,14.934 15.717,15.27 15.303,15.27H13.365C12.951,15.27 12.615,14.934 12.615,14.52Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M7.447,10.77C7.447,10.356 7.783,10.02 8.197,10.02H10.135C10.549,10.02 10.885,10.356 10.885,10.77C10.885,11.185 10.549,11.52 10.135,11.52H8.197C7.783,11.52 7.447,11.185 7.447,10.77Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M8.197,13.77C7.783,13.77 7.447,14.106 7.447,14.52C7.447,14.934 7.783,15.27 8.197,15.27H10.135C10.549,15.27 10.885,14.934 10.885,14.52C10.885,14.106 10.549,13.77 10.135,13.77H8.197Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M5.3,2.333C4.582,2.333 4,2.915 4,3.633V21.033C4,21.751 4.582,22.333 5.3,22.333H18.2C18.918,22.333 19.5,21.751 19.5,21.033V3.633C19.5,2.915 18.918,2.333 18.2,2.333H5.3ZM5.5,3.833V20.833H9.166V18.633C9.166,17.915 9.748,17.333 10.466,17.333H13.033C13.751,17.333 14.333,17.915 14.333,18.633V20.833H18V3.833H5.5Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
@@ -1223,4 +1223,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="use_chrome_autofill_integration">Use Chrome autofill integration</string>
|
||||
<string name="use_chrome_beta_autofill_integration">Use Chrome autofill integration (Beta)</string>
|
||||
<string name="improves_login_filling_for_supported_websites_on_chrome">Improves login filling for supported websites on Chrome. Once enabled, you’ll be directed to Chrome settings to enable third-party autofill.</string>
|
||||
<string name="show_more">Show more</string>
|
||||
<string name="no_folder">No folder</string>
|
||||
<string name="show_less">Show less</string>
|
||||
</resources>
|
||||
|
||||
@@ -45,6 +45,7 @@ fun createMockCipherView(
|
||||
cipherType: CipherType = CipherType.LOGIN,
|
||||
repromptType: CipherRepromptType = CipherRepromptType.NONE,
|
||||
totp: String? = "mockTotp-$number",
|
||||
organizationId: String? = "mockOrganizationId-$number",
|
||||
folderId: String? = "mockId-$number",
|
||||
clock: Clock = FIXED_CLOCK,
|
||||
fido2Credentials: List<Fido2Credential>? = null,
|
||||
@@ -52,7 +53,7 @@ fun createMockCipherView(
|
||||
): CipherView =
|
||||
CipherView(
|
||||
id = "mockId-$number",
|
||||
organizationId = "mockOrganizationId-$number",
|
||||
organizationId = organizationId,
|
||||
folderId = folderId,
|
||||
collectionIds = listOf("mockId-$number"),
|
||||
key = "mockKey-$number",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
@@ -23,6 +24,7 @@ import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onSiblings
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.R
|
||||
@@ -30,6 +32,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.x8bit.bitwarden.ui.util.assertNoPopupExists
|
||||
@@ -40,6 +43,7 @@ import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
@@ -49,6 +53,7 @@ import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -271,16 +276,16 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
mutableStateFlow.update { it.copy(viewState = typeState) }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Item name (required)")
|
||||
.assertTextContains("cipher")
|
||||
.onNodeWithText("cipher")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCommonContent(currentState) { copy(name = "Test Name") }
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Item name (required)")
|
||||
.assertTextContains("Test Name")
|
||||
.onNodeWithText("Test Name")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +318,168 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no folder should be displayed according to state`() {
|
||||
DEFAULT_VIEW_STATES.forEach { defaultViewState ->
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = defaultViewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("No folder")
|
||||
.assertIsDisplayed()
|
||||
|
||||
// Verify "No folder" is not displayed when relatedLocations is not empty
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = defaultViewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Collection("collection"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("No folder")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `organization locations should be displayed according to state`() {
|
||||
val organizationName = "My organization"
|
||||
DEFAULT_VIEW_STATES.forEach { viewState ->
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = viewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Organization(
|
||||
organizationName,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(organizationName)
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = viewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(organizationName)
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `collection locations should be displayed according to state`() {
|
||||
DEFAULT_VIEW_STATES.forEach { viewState ->
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = viewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Collection("My collection"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("My collection")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = viewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("My collection")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExpandingHeader should be displayed according to state`() {
|
||||
DEFAULT_VIEW_STATES.forEach { viewState ->
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = viewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Organization("My organization"),
|
||||
VaultItemLocation.Collection("My collection"),
|
||||
VaultItemLocation.Folder("My folder"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Show more")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Show less")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExpandingHeader should show expanded content according to state`() {
|
||||
DEFAULT_VIEW_STATES.forEach { viewState ->
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = viewState.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
relatedLocations = persistentListOf(
|
||||
VaultItemLocation.Organization("My organization"),
|
||||
VaultItemLocation.Collection("My collection"),
|
||||
VaultItemLocation.Folder("My folder"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Verify only two locations are shown when content is collapsed.
|
||||
composeTestRule
|
||||
.onNodeWithText("My folder")
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
// Verify all locations are show when content is expanded.
|
||||
composeTestRule
|
||||
.onNodeWithText("Show more")
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("My folder")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lastUpdated should be displayed according to state`() {
|
||||
EMPTY_VIEW_STATES
|
||||
@@ -1405,7 +1572,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTag("CipherNotesCopyButton")
|
||||
.performClick()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
|
||||
@@ -1569,7 +1736,11 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordVisibilityClicked(true))
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Login.PasswordVisibilityClicked(
|
||||
true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1735,7 +1906,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll("Copy TOTP")
|
||||
.performClick()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick)
|
||||
@@ -2142,7 +2313,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTag("IdentityCopyNameButton")
|
||||
.performClick()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick)
|
||||
@@ -2204,7 +2375,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTag("IdentityCopyPassportNumberButton")
|
||||
.performClick()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick)
|
||||
@@ -2215,11 +2386,10 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
@Test
|
||||
fun `in identity state, on copy license number field click should send CopyLicenseNumberClick`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
|
||||
// We scroll to email, which is right after the license number to avoid clicking on the FAB
|
||||
composeTestRule.onNodeWithTextAfterScroll("Email")
|
||||
composeTestRule.onNodeWithTextAfterScroll("License number")
|
||||
composeTestRule
|
||||
.onNodeWithTag("IdentityCopyLicenseNumberButton")
|
||||
.performClick()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyLicenseNumberClick)
|
||||
@@ -2248,7 +2418,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTag("IdentityCopyPhoneButton")
|
||||
.performClick()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick)
|
||||
@@ -2564,7 +2734,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll("Copy public key")
|
||||
.performClick()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPublicKeyClick)
|
||||
@@ -2583,7 +2753,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(privateKey)
|
||||
.onNodeWithTextAfterScroll(privateKey)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@@ -2738,6 +2908,8 @@ private val DEFAULT_STATE: VaultItemState = VaultItemState(
|
||||
cipherType = VaultItemCipherType.LOGIN,
|
||||
viewState = VaultItemState.ViewState.Loading,
|
||||
dialog = null,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
)
|
||||
|
||||
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
@@ -2778,6 +2950,8 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
favorite = false,
|
||||
iconData = IconData.Local(iconRes = R.drawable.ic_globe),
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
private val DEFAULT_PASSKEY = R.string.created_xy.asText(
|
||||
@@ -2863,6 +3037,8 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
favorite = false,
|
||||
iconData = IconData.Local(iconRes = R.drawable.ic_globe),
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login =
|
||||
@@ -2923,55 +3099,55 @@ private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
|
||||
private val EMPTY_IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
common = EMPTY_COMMON.copy(iconData = IconData.Local(R.drawable.ic_id_card)),
|
||||
type = EMPTY_IDENTITY_TYPE,
|
||||
)
|
||||
|
||||
private val EMPTY_CARD_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
common = EMPTY_COMMON.copy(iconData = IconData.Local(R.drawable.ic_payment_card)),
|
||||
type = EMPTY_CARD_TYPE,
|
||||
)
|
||||
|
||||
private val EMPTY_SECURE_NOTE_VIEW_STATE =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
common = EMPTY_COMMON.copy(iconData = IconData.Local(R.drawable.ic_note)),
|
||||
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
|
||||
)
|
||||
|
||||
private val EMPTY_SSH_KEY_VIEW_STATE =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
common = EMPTY_COMMON.copy(iconData = IconData.Local(R.drawable.ic_ssh_key)),
|
||||
type = EMPTY_SSH_KEY_TYPE,
|
||||
)
|
||||
|
||||
private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
type = DEFAULT_LOGIN,
|
||||
common = DEFAULT_COMMON,
|
||||
type = DEFAULT_LOGIN,
|
||||
)
|
||||
|
||||
private val DEFAULT_IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON.copy(iconData = IconData.Local(R.drawable.ic_id_card)),
|
||||
type = DEFAULT_IDENTITY,
|
||||
common = DEFAULT_COMMON,
|
||||
)
|
||||
|
||||
private val DEFAULT_CARD_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON.copy(iconData = IconData.Local(R.drawable.ic_payment_card)),
|
||||
type = DEFAULT_CARD,
|
||||
common = DEFAULT_COMMON,
|
||||
)
|
||||
|
||||
private val DEFAULT_SECURE_NOTE_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON,
|
||||
common = DEFAULT_COMMON.copy(iconData = IconData.Local(R.drawable.ic_note)),
|
||||
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
|
||||
)
|
||||
|
||||
private val DEFAULT_SSH_KEY_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON,
|
||||
common = DEFAULT_COMMON.copy(iconData = IconData.Local(R.drawable.ic_ssh_key)),
|
||||
type = DEFAULT_SSH_KEY,
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,19 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.util
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
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.Clock
|
||||
import java.time.Instant
|
||||
@@ -16,6 +26,16 @@ class CipherViewExtensionsTest {
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setupMockUri()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Login Content maintaining re-prompt and visibility state`() {
|
||||
@@ -48,6 +68,9 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -86,6 +109,9 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -117,6 +143,9 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -148,6 +177,9 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -185,6 +217,9 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -212,6 +247,9 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
@@ -237,11 +275,18 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true)
|
||||
common = createCommonContent(
|
||||
isEmpty = false,
|
||||
isPremiumUser = true,
|
||||
iconResId = R.drawable.ic_id_card,
|
||||
)
|
||||
.copy(currentCipher = cipherView),
|
||||
type = createIdentityContent(isEmpty = false),
|
||||
),
|
||||
@@ -261,11 +306,18 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = true, isPremiumUser = true)
|
||||
common = createCommonContent(
|
||||
isEmpty = true,
|
||||
isPremiumUser = true,
|
||||
iconResId = R.drawable.ic_id_card,
|
||||
)
|
||||
.copy(currentCipher = cipherView),
|
||||
type = createIdentityContent(isEmpty = true),
|
||||
),
|
||||
@@ -295,11 +347,18 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true)
|
||||
common = createCommonContent(
|
||||
isEmpty = false,
|
||||
isPremiumUser = true,
|
||||
iconResId = R.drawable.ic_id_card,
|
||||
)
|
||||
.copy(currentCipher = cipherView),
|
||||
type = createIdentityContent(
|
||||
isEmpty = false,
|
||||
@@ -334,11 +393,18 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true).copy(
|
||||
common = createCommonContent(
|
||||
isEmpty = false,
|
||||
isPremiumUser = true,
|
||||
iconResId = R.drawable.ic_id_card,
|
||||
).copy(
|
||||
currentCipher = cipherView.copy(
|
||||
identity = cipherView.identity?.copy(
|
||||
address1 = null,
|
||||
@@ -375,11 +441,18 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true)
|
||||
common = createCommonContent(
|
||||
isEmpty = false,
|
||||
isPremiumUser = true,
|
||||
iconResId = R.drawable.ic_note,
|
||||
)
|
||||
.copy(currentCipher = cipherView),
|
||||
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
|
||||
),
|
||||
@@ -400,10 +473,17 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
|
||||
val expectedState = VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = true, isPremiumUser = true)
|
||||
common = createCommonContent(
|
||||
isEmpty = true,
|
||||
isPremiumUser = true,
|
||||
iconResId = R.drawable.ic_note,
|
||||
)
|
||||
.copy(currentCipher = cipherView),
|
||||
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
|
||||
)
|
||||
@@ -423,10 +503,17 @@ class CipherViewExtensionsTest {
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = false, isPremiumUser = true).copy(
|
||||
common = createCommonContent(
|
||||
isEmpty = false,
|
||||
isPremiumUser = true,
|
||||
iconResId = R.drawable.ic_ssh_key,
|
||||
).copy(
|
||||
currentCipher = cipherView.copy(
|
||||
name = "mockName",
|
||||
sshKey = cipherView.sshKey?.copy(
|
||||
@@ -441,4 +528,43 @@ class CipherViewExtensionsTest {
|
||||
viewState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState with iconData based on cipher type`() {
|
||||
mapOf<CipherType, Int>(
|
||||
CipherType.LOGIN to R.drawable.ic_globe,
|
||||
CipherType.IDENTITY to R.drawable.ic_id_card,
|
||||
CipherType.CARD to R.drawable.ic_payment_card,
|
||||
CipherType.SECURE_NOTE to R.drawable.ic_note,
|
||||
CipherType.SSH_KEY to R.drawable.ic_ssh_key,
|
||||
)
|
||||
.forEach {
|
||||
val cipherView = createCipherView(type = it.key, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
)
|
||||
assertEquals(
|
||||
it.value,
|
||||
(viewState.asContentOrNull()?.common?.iconData as? IconData.Local)?.iconRes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMockUri() {
|
||||
mockkStatic(Uri::class)
|
||||
val uriMock = mockk<Uri>()
|
||||
every { Uri.parse(any()) } returns uriMock
|
||||
every { uriMock.host } returns "www.mockuri.com"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.util
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.vault.AttachmentView
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
@@ -14,9 +15,11 @@ import com.bitwarden.vault.SshKeyView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.time.Instant
|
||||
|
||||
const val DEFAULT_IDENTITY_NAME: String = "Mr firstName middleName lastName"
|
||||
@@ -160,6 +163,7 @@ fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView =
|
||||
fun createCommonContent(
|
||||
isEmpty: Boolean,
|
||||
isPremiumUser: Boolean,
|
||||
@DrawableRes iconResId: Int = R.drawable.ic_globe,
|
||||
): VaultItemState.ViewState.Content.Common =
|
||||
if (isEmpty) {
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
@@ -174,6 +178,8 @@ fun createCommonContent(
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
favorite = false,
|
||||
relatedLocations = persistentListOf(),
|
||||
iconData = IconData.Local(iconResId),
|
||||
)
|
||||
} else {
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
@@ -221,6 +227,8 @@ fun createCommonContent(
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
favorite = false,
|
||||
relatedLocations = persistentListOf(),
|
||||
iconData = IconData.Local(iconResId),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user