From 7804d8430f8058651b08abb53f399cde4a57ac4e Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Fri, 21 Feb 2025 18:19:25 -0500 Subject: [PATCH 01/27] [PM-18067] Consolidate item name fields into `ItemHeader` This commit introduces `ItemHeader`, a new composable that replaces `ItemNameField` to display the item name, favorite status, and related details like organization, collections, and folder. Key changes: - Removes `ItemNameField` - Adds `ItemHeader` for displaying item name and favorite status, along with item location information. - Introduces a new `ic_organization` icon. - Adds the logic for showing item locations (organization, collections, folders) in a collapsible view. - Removes `ItemNameField` from `VaultItemLoginContent`, `VaultItemIdentityContent`, `VaultItemSecureNoteContent`, `VaultItemCardContent`, `VaultItemSshKeyContent` and replace it with `ItemHeader` - Adds the logic to fetch and display the item icon in `ItemHeader` based on item type - Adds an `ItemLocationListItem` for displaying location details. - Adds a `VaultItemLocation` data class for representing item locations. - Adds new `baseIconUrl` and `isIconLoadingDisabled` variables to the `VaultItemState` to handle icon display. - Updates `CipherView.toIconData` to handle the item icon. - Adds new `show_more`, `no_folder` and `show_less` string resources. - Updates the `BitwardenShapes` to include `favicon` shapes. - Updates the `BitwardenColorScheme` to include `faviconForeground` and `faviconBackground`. - Updates `BitwardenExpandingHeader` to include expandedText, collapsedText and showExpansionIndicator properties. --- .../header/BitwardenExpandingHeader.kt | 32 +- .../theme/color/BitwardenColorScheme.kt | 2 + .../ui/platform/theme/color/ColorScheme.kt | 6 + .../platform/theme/shape/BitwardenShapes.kt | 1 + .../ui/platform/theme/shape/Shapes.kt | 1 + .../feature/item/VaultItemCardContent.kt | 7 +- .../feature/item/VaultItemIdentityContent.kt | 9 +- .../feature/item/VaultItemLoginContent.kt | 19 +- .../item/VaultItemSecureNoteContent.kt | 9 +- .../feature/item/VaultItemSshKeyContent.kt | 7 +- .../vault/feature/item/VaultItemViewModel.kt | 92 +++- .../feature/item/component/ItemHeader.kt | 368 +++++++++++++++ .../feature/item/component/ItemNameField.kt | 49 -- .../feature/item/model/VaultItemLocation.kt | 59 +++ .../feature/item/model/VaultItemStateData.kt | 2 + .../feature/item/util/CipherViewExtensions.kt | 41 ++ app/src/main/res/drawable/ic_organization.xml | 28 ++ app/src/main/res/values/strings.xml | 3 + .../datasource/sdk/model/CipherViewUtil.kt | 3 +- .../vault/feature/item/VaultItemScreenTest.kt | 221 ++++++++- .../feature/item/VaultItemViewModelTest.kt | 433 +++++++++++++++++- .../item/util/CipherViewExtensionsTest.kt | 140 +++++- .../feature/item/util/VaultItemTestUtil.kt | 7 + 23 files changed, 1388 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt delete mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemNameField.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemLocation.kt create mode 100644 app/src/main/res/drawable/ic_organization.xml diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt index a12c4a628e..b07e80f5ce 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt @@ -21,13 +21,19 @@ 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, ) { Row( modifier = modifier @@ -45,20 +51,22 @@ fun BitwardenExpandingHeader( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = title, + text = if (isExpanded) expandedText else collapsedText, 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), - ) + 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), + ) + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt index ce9b26e9ac..aa469e50ba 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt @@ -64,6 +64,8 @@ data class BitwardenColorScheme( val reversed: Color, val badgeBackground: Color, val badgeForeground: Color, + val faviconForeground: Color, + val faviconBackground: Color, ) /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt index 15de7e7d6e..9b50b87185 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt @@ -34,6 +34,8 @@ val darkBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme( reversed = PrimitiveColors.gray1100, badgeBackground = PrimitiveColors.pink200, badgeForeground = PrimitiveColors.gray1100, + faviconForeground = PrimitiveColors.blue500, + faviconBackground = PrimitiveColors.blue200, ), filledButton = BitwardenColorScheme.FilledButtonColors( background = PrimitiveColors.blue400, @@ -102,6 +104,8 @@ val lightBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme( reversed = PrimitiveColors.gray100, badgeBackground = PrimitiveColors.pink100, badgeForeground = PrimitiveColors.gray100, + faviconForeground = PrimitiveColors.blue700, + faviconBackground = PrimitiveColors.blue100, ), filledButton = BitwardenColorScheme.FilledButtonColors( background = PrimitiveColors.blue500, @@ -176,6 +180,8 @@ fun dynamicBitwardenColorScheme( reversed = materialColorScheme.inversePrimary, badgeBackground = materialColorScheme.error, badgeForeground = materialColorScheme.onError, + faviconForeground = materialColorScheme.onTertiaryContainer, + faviconBackground = materialColorScheme.tertiaryContainer, ), filledButton = BitwardenColorScheme.FilledButtonColors( background = materialColorScheme.primary, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt index 0bcb0251dc..671b8a51c2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt @@ -22,4 +22,5 @@ data class BitwardenShapes( val progressIndicator: CornerBasedShape, val segmentedControl: CornerBasedShape, val snackbar: CornerBasedShape, + val favicon: CornerBasedShape, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt index 28f2e40d08..55689d1d6b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt @@ -22,4 +22,5 @@ val bitwardenShapes: BitwardenShapes = BitwardenShapes( progressIndicator = CircleShape, segmentedControl = CircleShape, snackbar = RoundedCornerShape(size = 8.dp), + favicon = CircleShape, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index d303849f37..ab08298838 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -22,7 +22,7 @@ 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.ItemHeader import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers @@ -44,9 +44,12 @@ fun VaultItemCardContent( LazyColumn(modifier = modifier) { item { Spacer(modifier = Modifier.height(height = 12.dp)) - ItemNameField( + ItemHeader( value = commonState.name, isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "CardItemNameIcon", textFieldTestTag = "CardItemNameEntry", modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index 2ee1d49383..5aca1a9bed 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -21,7 +21,7 @@ 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.ItemHeader import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers @@ -41,10 +41,13 @@ fun VaultItemIdentityContent( LazyColumn(modifier = modifier) { item { Spacer(modifier = Modifier.height(height = 12.dp)) - ItemNameField( + ItemHeader( value = commonState.name, isFavorite = commonState.favorite, - textFieldTestTag = "ItemNameEntry", + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "IdentityItemNameIcon", + textFieldTestTag = "IdentityItemNameEntry", modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 3293b30bdc..51a5f27325 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -30,7 +30,7 @@ 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.ItemHeader import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers @@ -54,20 +54,13 @@ fun VaultItemLoginContent( modifier = modifier, ) { 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)) - } - item { - ItemNameField( + Spacer(Modifier.height(24.dp)) + ItemHeader( value = commonState.name, isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "LoginItemNameIcon", textFieldTestTag = "LoginItemNameEntry", modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index 8561cc6747..ed420e4293 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -25,7 +25,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 /** @@ -41,10 +41,13 @@ fun VaultItemSecureNoteContent( LazyColumn(modifier = modifier) { item { Spacer(modifier = Modifier.height(height = 12.dp)) - ItemNameField( + ItemHeader( value = commonState.name, isFavorite = commonState.favorite, - textFieldTestTag = "ItemNameEntry", + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "SecureNoteItemNameIcon", + textFieldTestTag = "SecureNoteItemNameEntry", modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt index 8957bf3ed5..5536345183 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt @@ -22,7 +22,7 @@ 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.ItemHeader import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers @@ -53,9 +53,12 @@ fun VaultItemSshKeyContent( } item { - ItemNameField( + ItemHeader( value = commonState.name, isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "SshKeyItemNameIcon", textFieldTestTag = "SshKeyItemNameEntry", modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index aa5024adf7..8bec1b44f2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -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 @@ -37,6 +42,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import dagger.hilt.android.lifecycle.HiltViewModel 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 +57,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 +66,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( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { @@ -69,6 +77,8 @@ class VaultItemViewModel @Inject constructor( cipherType = args.cipherType, viewState = VaultItemState.ViewState.Loading, dialog = null, + baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled, ) }, ) { @@ -91,7 +101,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 +117,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 = buildList { + organizationName?.let { add(VaultItemLocation.Organization(it)) } + addAll(collections.map { VaultItemLocation.Collection(it) }) + folderName?.let { add(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 +1090,10 @@ class VaultItemViewModel @Inject constructor( is VaultItemAction.Internal.AttachmentFinishedSavingToDisk -> { handleAttachmentFinishedSavingToDisk(action) } + + is VaultItemAction.Internal.IsIconLoadingDisabledUpdateReceive -> { + handleIsIconLoadingDisabledUpdateReceive(action) + } } } @@ -1147,6 +1195,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(), ) ?: VaultItemState.ViewState.Error(message = errorText) @@ -1285,6 +1336,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 +1430,8 @@ data class VaultItemState( val cipherType: VaultItemCipherType, val viewState: ViewState, val dialog: DialogState?, + val baseIconUrl: String, + val isIconLoadingDisabled: Boolean, ) : Parcelable { /** @@ -1487,7 +1546,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 +1563,8 @@ data class VaultItemState( val canAssignToCollections: Boolean, val canEdit: Boolean, val favorite: Boolean, + val iconData: IconData, + val relatedLocations: List, ) : Parcelable { /** @@ -2248,6 +2309,13 @@ sealed class VaultItemAction { val isSaved: Boolean, val file: File, ) : Internal() + + /** + * Indicates the `isIconLoadingDisabled` setting has changed. + */ + data class IsIconLoadingDisabledUpdateReceive( + val isDisabled: Boolean, + ) : Internal() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt new file mode 100644 index 0000000000..953f203b6d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -0,0 +1,368 @@ +package com.x8bit.bitwarden.ui.vault.feature.item.component + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +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.data.platform.annotation.OmitFromCoverage +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 + +/** + * 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 and favorite status. + */ +@OmitFromCoverage +@Suppress("LongMethod") +@Composable +fun ItemHeader( + value: String, + isFavorite: Boolean, + relatedLocations: List, + iconData: IconData, + modifier: Modifier = Modifier, + iconTestTag: String? = null, + textFieldTestTag: String? = null, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(shape = BitwardenTheme.shapes.content) + .background(color = BitwardenTheme.colorScheme.background.secondary), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) { + 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 = CardStyle.Full, + textStyle = BitwardenTheme.typography.titleMedium, + ) + } + + if (relatedLocations.isEmpty()) { + ItemLocationListItem( + vectorPainter = rememberVectorPainter(R.drawable.ic_folder), + text = stringResource(R.string.no_folder), + iconTestTag = "NoFolderIcon", + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + return@Column + } + + relatedLocations + .take(EXPANDABLE_THRESHOLD) + .forEach { + ItemLocationListItem( + vectorPainter = rememberVectorPainter(it.icon), + iconTestTag = "ItemLocationIcon", + text = it.name, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + } + + ExpandingItemLocationContent( + overflowLocations = relatedLocations.drop(EXPANDABLE_THRESHOLD), + ) + } +} + +@Composable +private fun ExpandingItemLocationContent( + overflowLocations: List, +) { + var isExpanded by rememberSaveable { mutableStateOf(false) } + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically(), + modifier = Modifier.clipToBounds(), + ) { + Column { + overflowLocations + .forEach { + ItemLocationListItem( + vectorPainter = rememberVectorPainter(it.icon), + text = it.name, + iconTestTag = "ItemLocationIcon", + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + } + } + } + + if (overflowLocations.isNotEmpty()) { + BitwardenExpandingHeader( + collapsedText = stringResource(R.string.show_more), + expandedText = stringResource(R.string.show_less), + isExpanded = isExpanded, + onClick = { isExpanded = !isExpanded }, + showExpansionIndicator = false, + ) + } +} + +@Composable +private fun ItemHeaderIcon( + iconData: IconData, + modifier: Modifier = Modifier, + testTag: String? = null, +) { + Box( + contentAlignment = Alignment.Center, + modifier = if (iconData is IconData.Local) { + modifier.then( + Modifier.background( + color = BitwardenTheme.colorScheme.icon.faviconBackground, + shape = BitwardenTheme.shapes.favicon, + ), + ) + } else { + modifier + }, + ) { + BitwardenIcon( + iconData = iconData, + contentDescription = null, + tint = BitwardenTheme.colorScheme.icon.faviconForeground, + modifier = Modifier + .nullableTestTag(testTag), + ) + } +} + +@Composable +private fun 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(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun ItemHeader_LocalIcon_Preview() { + BitwardenTheme { + LazyColumn { + item { + ItemHeader( + value = "Login without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_globe, + ), + relatedLocations = emptyList(), + ) + } + } + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun ItemHeader_NetworkIcon_Preview() { + BitwardenTheme { + LazyColumn { + item { + ItemHeader( + value = "Login with favicon", + isFavorite = true, + iconData = IconData.Network( + uri = "mockuri", + fallbackIconRes = R.drawable.ic_globe, + ), + relatedLocations = emptyList(), + ) + } + } + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun ItemHeader_Organization_Preview() { + BitwardenTheme { + LazyColumn { + item { + ItemHeader( + value = "Login without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_globe, + ), + relatedLocations = listOf( + VaultItemLocation.Organization("Stark Industries"), + ), + ) + } + } + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun ItemNameField_Org_SingleCollection_Preview() { + BitwardenTheme { + LazyColumn { + item { + ItemHeader( + value = "Login without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_globe, + ), + relatedLocations = listOf( + VaultItemLocation.Organization("Stark Industries"), + VaultItemLocation.Collection("Marketing"), + ), + ) + } + } + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun ItemNameField_Org_MultiCollection_Preview() { + BitwardenTheme { + LazyColumn { + item { + ItemHeader( + value = "Login without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_globe, + ), + relatedLocations = listOf( + VaultItemLocation.Organization("Stark Industries"), + VaultItemLocation.Collection("Marketing"), + VaultItemLocation.Collection("Product"), + ), + ) + } + } + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun ItemNameField_Org_SingleCollection_Folder_Preview() { + BitwardenTheme { + LazyColumn { + item { + ItemHeader( + value = "Note without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_note, + ), + relatedLocations = listOf( + VaultItemLocation.Organization("Stark Industries"), + VaultItemLocation.Collection("Marketing"), + VaultItemLocation.Folder("Competition"), + ), + ) + } + } + } +} +//endregion Previews diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemNameField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemNameField.kt deleted file mode 100644 index d24ea18818..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemNameField.kt +++ /dev/null @@ -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, - ) -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemLocation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemLocation.kt new file mode 100644 index 0000000000..26d464207a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemLocation.kt @@ -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 + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt index f89e0f93f2..bb61678f6e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt @@ -10,6 +10,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 +18,5 @@ data class VaultItemStateData( val canDelete: Boolean, val canAssociateToCollections: Boolean, val canEdit: Boolean, + val relatedLocations: List, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index cf4f230c30..0703857f85 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -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,9 +18,12 @@ 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 @@ -42,6 +46,9 @@ fun CipherView.toViewState( canDelete: Boolean, canAssignToCollections: Boolean, canEdit: Boolean, + baseIconUrl: String, + isIconLoadingDisabled: Boolean, + relatedLocations: List, ): VaultItemState.ViewState = VaultItemState.ViewState.Content( common = VaultItemState.ViewState.Content.Common( @@ -86,6 +93,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 +238,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, diff --git a/app/src/main/res/drawable/ic_organization.xml b/app/src/main/res/drawable/ic_organization.xml new file mode 100644 index 0000000000..e84e4015f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_organization.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e35128786e..ec600ce257 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1223,4 +1223,7 @@ Do you want to switch to this account? Use Chrome autofill integration Use Chrome autofill integration (Beta) Improves login filling for supported websites on Chrome. Once enabled, you’ll be directed to Chrome settings to enable third-party autofill. + Show more + No folder + Show less diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index 3498970ac7..f33e0a4eee 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -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? = 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", diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index bbd069b463..9078d17685 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -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 @@ -271,16 +275,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 +317,166 @@ 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 = emptyList(), + ), + ), + ) + } + 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 = listOf(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 = listOf( + VaultItemLocation.Organization( + organizationName, + ), + ), + ), + ), + ) + } + composeTestRule + .onNodeWithText(organizationName) + .assertIsDisplayed() + + mutableStateFlow.update { + DEFAULT_STATE.copy( + viewState = viewState.copy( + common = DEFAULT_COMMON.copy( + relatedLocations = emptyList(), + ), + ), + ) + } + 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 = listOf( + VaultItemLocation.Collection("My collection"), + ), + ), + ), + ) + } + composeTestRule + .onNodeWithText("My collection") + .assertIsDisplayed() + + mutableStateFlow.update { + DEFAULT_STATE.copy( + viewState = viewState.copy( + common = DEFAULT_COMMON.copy( + relatedLocations = emptyList(), + ), + ), + ) + } + 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 = listOf( + 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 = listOf( + 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 +1569,7 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule .onNodeWithTag("CipherNotesCopyButton") - .performClick() + .performSemanticsAction(SemanticsActions.OnClick) verify { viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick) @@ -1569,7 +1733,11 @@ class VaultItemScreenTest : BaseComposeTest() { .performClick() verify(exactly = 1) { - viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordVisibilityClicked(true)) + viewModel.trySendAction( + VaultItemAction.ItemType.Login.PasswordVisibilityClicked( + true, + ), + ) } } @@ -1735,7 +1903,7 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule .onNodeWithContentDescriptionAfterScroll("Copy TOTP") - .performClick() + .performSemanticsAction(SemanticsActions.OnClick) verify { viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick) @@ -2142,7 +2310,7 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule .onNodeWithTag("IdentityCopyNameButton") - .performClick() + .performSemanticsAction(SemanticsActions.OnClick) verify { viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick) @@ -2204,7 +2372,7 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule .onNodeWithTag("IdentityCopyPassportNumberButton") - .performClick() + .performSemanticsAction(SemanticsActions.OnClick) verify { viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick) @@ -2215,11 +2383,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 +2415,7 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule .onNodeWithTag("IdentityCopyPhoneButton") - .performClick() + .performSemanticsAction(SemanticsActions.OnClick) verify { viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick) @@ -2564,7 +2731,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 +2750,7 @@ class VaultItemScreenTest : BaseComposeTest() { ) } composeTestRule - .onNodeWithText(privateKey) + .onNodeWithTextAfterScroll(privateKey) .assertIsDisplayed() } @@ -2738,6 +2905,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 +2947,8 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = canAssignToCollections = true, canEdit = true, favorite = false, + iconData = IconData.Local(iconRes = R.drawable.ic_globe), + relatedLocations = emptyList(), ) private val DEFAULT_PASSKEY = R.string.created_xy.asText( @@ -2863,6 +3034,8 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common = canAssignToCollections = true, canEdit = true, favorite = false, + iconData = IconData.Local(iconRes = R.drawable.ic_globe), + relatedLocations = emptyList(), ) private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login = @@ -2923,55 +3096,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, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 8b5b097572..560a6f1ece 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -5,19 +5,28 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.vault.CipherView import com.bitwarden.vault.CollectionView +import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult +import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.UserState 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.FirstTimeState 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.model.Environment +import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository +import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl +import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -28,7 +37,9 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest 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.vault.feature.item.model.TotpCodeItemData +import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation import com.x8bit.bitwarden.ui.vault.feature.item.util.createCommonContent import com.x8bit.bitwarden.ui.vault.feature.item.util.createLoginContent import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState @@ -64,6 +75,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { private val mutableUserStateFlow = MutableStateFlow(DEFAULT_USER_STATE) private val mutableCollectionsStateFlow = MutableStateFlow>>(DataState.Loading) + private val mutableFoldersStateFlow = + MutableStateFlow>>(DataState.Loading) private val clipboardManager: BitwardenClipboardManager = mockk { every { setText(text = any(), toastDescriptorOverride = any()) } just runs @@ -75,6 +88,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { every { getAuthCodeFlow(VAULT_ITEM_ID) } returns mutableAuthCodeItemFlow every { getVaultItemStateFlow(VAULT_ITEM_ID) } returns mutableVaultItemFlow every { collectionsStateFlow } returns mutableCollectionsStateFlow + every { foldersStateFlow } returns mutableFoldersStateFlow } private val mockFileManager: FileManager = mockk() @@ -85,6 +99,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { private val mockCipherView = mockk { every { collectionIds } returns emptyList() every { edit } returns true + every { folderId } returns null + every { organizationId } returns null + } + private val mockEnvironmentRepository = FakeEnvironmentRepository() + private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false) + private val mockSettingsRepository = mockk { + every { isIconLoadingDisabled } returns false + every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow } @BeforeEach @@ -95,6 +117,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { @AfterEach fun tearDown() { unmockkStatic(CipherView::toViewState) + unmockkStatic(Uri::class) } @Test @@ -176,11 +199,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.DeleteClick) @@ -202,6 +229,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -222,6 +252,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginState @@ -239,6 +272,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.Common.DeleteClick) assertEquals(expected, viewModel.stateFlow.value) @@ -267,6 +301,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginState @@ -280,6 +317,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.Common.DeleteClick) assertEquals(expected, viewModel.stateFlow.value) @@ -301,12 +339,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -346,11 +388,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -394,12 +440,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -434,11 +484,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) val viewModel = createViewModel(state = loginState) assertEquals(loginState, viewModel.stateFlow.value) @@ -469,12 +523,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns viewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = viewState) val viewModel = createViewModel(state = loginState) assertEquals(loginState, viewModel.stateFlow.value) @@ -508,6 +566,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -515,6 +576,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { data = createVerificationCodeItem(), ) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -550,11 +612,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -601,11 +667,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) val viewModel = createViewModel(state = loginState) assertEquals(loginState, viewModel.stateFlow.value) @@ -635,11 +705,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -654,6 +728,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } assertEquals( @@ -683,6 +760,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState @@ -693,6 +773,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -748,6 +829,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState @@ -758,6 +842,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -804,6 +889,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState @@ -814,6 +902,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -871,11 +960,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick("field")) @@ -897,6 +990,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -914,6 +1010,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) every { clipboardManager.setText(text = field) } just runs @@ -921,6 +1020,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick(field)) @@ -934,6 +1034,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) organizationEventManager.trackEvent( event = OrganizationEvent.CipherClientCopiedHiddenField( @@ -975,11 +1078,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -1009,6 +1116,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -1044,11 +1154,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -1078,6 +1192,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) organizationEventManager.trackEvent( event = OrganizationEvent.CipherClientToggledHiddenFieldVisible( @@ -1100,11 +1217,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.AttachmentsClick) @@ -1126,6 +1247,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -1147,11 +1271,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1183,11 +1311,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1211,6 +1343,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -1236,11 +1371,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.CloneClick) @@ -1286,11 +1425,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.CloneClick) @@ -1312,6 +1455,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -1333,11 +1479,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1367,11 +1517,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.MoveToOrganizationClick) @@ -1393,6 +1547,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -1414,11 +1571,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1458,11 +1619,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -1516,11 +1681,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -1583,11 +1752,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -1751,12 +1924,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val notes = "Lots of notes" @@ -1793,11 +1970,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) val breachCount = 5 @@ -1835,6 +2016,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } coVerify(exactly = 1) { @@ -1855,12 +2039,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick) @@ -1884,6 +2072,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -1901,12 +2092,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick) @@ -1923,19 +2118,28 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @Test fun `on CopyTotpClick should call setText on the ClipboardManager`() = runTest { + setupMockUri() mutableVaultItemFlow.value = DataState.Loaded( data = createMockCipherView(1), ) mutableAuthCodeItemFlow.value = DataState.Loaded( data = createVerificationCodeItem(), ) - mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableCollectionsStateFlow.value = DataState.Loaded( + data = listOf(createMockCollectionView(1)), + ) + mutableFoldersStateFlow.value = DataState.Loaded( + data = listOf(createMockFolderView(1)), + ) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick) @@ -1970,12 +2174,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick) @@ -1992,6 +2200,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2033,12 +2244,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick) @@ -2060,6 +2275,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2077,6 +2295,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } .returns( @@ -2088,6 +2309,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.eventFlow.test { viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick) @@ -2106,6 +2328,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2124,12 +2349,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2155,6 +2384,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2176,12 +2408,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2210,6 +2446,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) organizationEventManager.trackEvent( event = OrganizationEvent.CipherClientToggledPasswordVisible( @@ -2246,11 +2485,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick) @@ -2274,6 +2517,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2291,6 +2537,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2299,6 +2548,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick) @@ -2315,6 +2565,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2332,11 +2585,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2360,6 +2617,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2377,6 +2637,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2385,6 +2648,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction( VaultItemAction.ItemType.Card.NumberVisibilityClick(isVisible = true), @@ -2404,6 +2668,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2421,11 +2688,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick) @@ -2449,6 +2720,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2466,6 +2740,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2474,6 +2751,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick) @@ -2490,6 +2768,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2507,11 +2788,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2535,6 +2820,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2551,6 +2839,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2559,6 +2850,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableFoldersStateFlow.value = DataState.Loaded(data = emptyList()) viewModel.trySendAction( VaultItemAction.ItemType.Card.CodeVisibilityClick(isVisible = true), @@ -2578,6 +2870,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2607,11 +2902,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPublicKeyClick) @@ -2641,11 +2940,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns sshKeyViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(sshKeyState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2671,6 +2974,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2689,11 +2995,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(sshKeyState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2718,6 +3028,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } } @@ -2735,6 +3048,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2743,6 +3059,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick) @@ -2767,11 +3084,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick) @@ -2804,11 +3125,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyFingerprintClick) @@ -2841,11 +3166,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns IDENTITY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) } @Test @@ -2980,6 +3309,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with Loaded and nonnull data should update the ViewState`() { val viewState = mockk() + every { mockCipherView.organizationId } returns "mockOrganizationId" + every { mockCipherView.collectionIds } returns listOf("mockId-1") + every { mockCipherView.folderId } returns "mockId-1" every { mockCipherView.toViewState( previousState = null, @@ -2989,12 +3321,40 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = listOf( + VaultItemLocation.Organization("mockOrganizationName"), + VaultItemLocation.Collection("mockName-1"), + VaultItemLocation.Folder("mockName-1"), + ), ) } returns viewState + mutableUserStateFlow.value = DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_USER_ACCOUNT.copy( + organizations = listOf( + Organization( + id = "mockOrganizationId", + name = "mockOrganizationName", + shouldManageResetPassword = false, + shouldUseKeyConnector = false, + role = OrganizationType.OWNER, + ), + ), + ), + ), + ) + val viewModel = createViewModel(state = null) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableCollectionsStateFlow.value = DataState.Loaded( + listOf(createMockCollectionView(number = 1)), + ) + mutableFoldersStateFlow.value = DataState.Loaded( + listOf(createMockFolderView(number = 1)), + ) assertEquals( DEFAULT_STATE.copy(viewState = viewState), @@ -3008,6 +3368,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals( DEFAULT_STATE.copy( @@ -3031,12 +3392,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns viewState val viewModel = createViewModel(state = null) mutableVaultItemFlow.value = DataState.Pending(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) } @@ -3048,6 +3413,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Pending(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + mutableFoldersStateFlow.value = DataState.Loaded(emptyList()) assertEquals( DEFAULT_STATE.copy( @@ -3071,6 +3437,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns viewState val viewModel = createViewModel(state = null) @@ -3108,6 +3477,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = emptyList(), ) } returns viewState val viewModel = createViewModel(state = null) @@ -3160,6 +3532,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { fileManager: FileManager = mockFileManager, eventManager: OrganizationEventManager = organizationEventManager, tempAttachmentFile: File? = null, + environmentRepository: EnvironmentRepository = mockEnvironmentRepository, + settingsRepository: SettingsRepository = mockSettingsRepository, ): VaultItemViewModel = VaultItemViewModel( savedStateHandle = SavedStateHandle().apply { set("state", state) @@ -3181,6 +3555,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { vaultRepository = vaultRepository, fileManager = fileManager, organizationEventManager = eventManager, + environmentRepository = environmentRepository, + settingsRepository = settingsRepository, ) private fun createViewState( @@ -3200,6 +3576,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCode = "mockTotp-1", ) + private fun setupMockUri() { + mockkStatic(Uri::class) + val uriMock = mockk() + every { Uri.parse(any()) } returns uriMock + every { uriMock.host } returns "www.mockuri.com" + } + companion object { private const val VAULT_ITEM_ID = "vault_item_id" private const val DEFAULT_LOGIN_PASSWORD = "password" @@ -3210,31 +3593,33 @@ class VaultItemViewModelTest : BaseViewModelTest() { cipherType = VaultItemCipherType.LOGIN, viewState = VaultItemState.ViewState.Loading, dialog = null, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + ) + + private val DEFAULT_USER_ACCOUNT = UserState.Account( + userId = "user_id_1", + name = "Bit", + email = "bitwarden@gmail.com", + avatarColorHex = "#ff00ff", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = FirstTimeState(showImportLoginsCard = true), ) private val DEFAULT_USER_STATE: UserState = UserState( activeUserId = "user_id_1", - accounts = listOf( - UserState.Account( - userId = "user_id_1", - name = "Bit", - email = "bitwarden@gmail.com", - avatarColorHex = "#ff00ff", - environment = Environment.Us, - isPremium = true, - isLoggedIn = true, - isVaultUnlocked = true, - needsPasswordReset = false, - isBiometricsEnabled = false, - organizations = emptyList(), - needsMasterPassword = false, - trustedDevice = null, - hasMasterPassword = true, - isUsingKeyConnector = false, - onboardingStatus = OnboardingStatus.COMPLETE, - firstTimeState = FirstTimeState(showImportLoginsCard = true), - ), - ), + accounts = listOf(DEFAULT_USER_ACCOUNT), ) private val DEFAULT_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login = @@ -3350,6 +3735,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, canEdit = true, favorite = false, + iconData = IconData.Local(R.drawable.ic_globe), + relatedLocations = emptyList(), ) private val DEFAULT_VIEW_STATE: VaultItemState.ViewState.Content = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt index a9b2d277f2..0cc6e3e401 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt @@ -1,9 +1,18 @@ 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 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 +25,17 @@ class CipherViewExtensionsTest { ZoneOffset.UTC, ) + @BeforeEach + fun setUp() { + mockkStatic(Uri::class) + 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 = emptyList(), ) assertEquals( @@ -86,6 +109,9 @@ class CipherViewExtensionsTest { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = "https://example.com/", + isIconLoadingDisabled = true, + relatedLocations = emptyList(), ) assertEquals( @@ -117,6 +143,9 @@ class CipherViewExtensionsTest { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = "https://example.com/", + isIconLoadingDisabled = true, + relatedLocations = emptyList(), ) assertEquals( @@ -148,6 +177,9 @@ class CipherViewExtensionsTest { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = "https://example.com/", + isIconLoadingDisabled = true, + relatedLocations = emptyList(), ) assertEquals( @@ -185,6 +217,9 @@ class CipherViewExtensionsTest { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = "https://example.com/", + isIconLoadingDisabled = true, + relatedLocations = emptyList(), ) assertEquals( @@ -212,6 +247,9 @@ class CipherViewExtensionsTest { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = "https://example.com/", + isIconLoadingDisabled = true, + relatedLocations = emptyList(), ) assertEquals( @@ -237,11 +275,18 @@ class CipherViewExtensionsTest { canDelete = true, canAssignToCollections = true, canEdit = true, + baseIconUrl = "https://example.com/", + isIconLoadingDisabled = true, + relatedLocations = emptyList(), ) 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 = emptyList(), ) 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 = emptyList(), ) 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 = emptyList(), ) 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 = emptyList(), ) 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 = emptyList(), ) 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 = emptyList(), ) 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.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 = emptyList(), + ) + assertEquals( + it.value, + (viewState.asContentOrNull()?.common?.iconData as? IconData.Local)?.iconRes, + ) + } + } + + private fun setupMockUri() { + mockkStatic(Uri::class) + val uriMock = mockk() + every { Uri.parse(any()) } returns uriMock + every { uriMock.host } returns "www.mockuri.com" + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index 207c3bdb65..57b99e2af7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -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,6 +15,7 @@ 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 @@ -160,6 +162,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 +177,8 @@ fun createCommonContent( canAssignToCollections = true, canEdit = true, favorite = false, + relatedLocations = emptyList(), + iconData = IconData.Local(iconResId), ) } else { VaultItemState.ViewState.Content.Common( @@ -221,6 +226,8 @@ fun createCommonContent( canAssignToCollections = true, canEdit = true, favorite = false, + relatedLocations = emptyList(), + iconData = IconData.Local(iconResId), ) } From 60da236f3e405479c72bd983f2d413b3b05d8d3b Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 16:44:51 -0500 Subject: [PATCH 02/27] Add `illustration` colors --- .../theme/color/BitwardenColorScheme.kt | 12 ++++++++++-- .../ui/platform/theme/color/ColorScheme.kt | 18 ++++++++++++------ .../vault/feature/item/component/ItemHeader.kt | 4 ++-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt index aa469e50ba..421a32a314 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/BitwardenColorScheme.kt @@ -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. @@ -64,8 +65,6 @@ data class BitwardenColorScheme( val reversed: Color, val badgeBackground: Color, val badgeForeground: Color, - val faviconForeground: Color, - val faviconBackground: Color, ) /** @@ -126,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, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt index 9b50b87185..2f9af64a9d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/color/ColorScheme.kt @@ -34,8 +34,6 @@ val darkBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme( reversed = PrimitiveColors.gray1100, badgeBackground = PrimitiveColors.pink200, badgeForeground = PrimitiveColors.gray1100, - faviconForeground = PrimitiveColors.blue500, - faviconBackground = PrimitiveColors.blue200, ), filledButton = BitwardenColorScheme.FilledButtonColors( background = PrimitiveColors.blue400, @@ -71,6 +69,10 @@ val darkBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme( weak2 = PrimitiveColors.yellow200, error = PrimitiveColors.red200, ), + illustration = BitwardenColorScheme.IllustrationColors( + outline = PrimitiveColors.blue500, + backgroundPrimary = PrimitiveColors.blue200, + ), ) /** @@ -104,8 +106,6 @@ val lightBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme( reversed = PrimitiveColors.gray100, badgeBackground = PrimitiveColors.pink100, badgeForeground = PrimitiveColors.gray100, - faviconForeground = PrimitiveColors.blue700, - faviconBackground = PrimitiveColors.blue100, ), filledButton = BitwardenColorScheme.FilledButtonColors( background = PrimitiveColors.blue500, @@ -141,6 +141,10 @@ val lightBitwardenColorScheme: BitwardenColorScheme = BitwardenColorScheme( weak2 = PrimitiveColors.yellow300, error = PrimitiveColors.red300, ), + illustration = BitwardenColorScheme.IllustrationColors( + outline = PrimitiveColors.blue700, + backgroundPrimary = PrimitiveColors.blue100, + ), ) /** @@ -180,8 +184,6 @@ fun dynamicBitwardenColorScheme( reversed = materialColorScheme.inversePrimary, badgeBackground = materialColorScheme.error, badgeForeground = materialColorScheme.onError, - faviconForeground = materialColorScheme.onTertiaryContainer, - faviconBackground = materialColorScheme.tertiaryContainer, ), filledButton = BitwardenColorScheme.FilledButtonColors( background = materialColorScheme.primary, @@ -217,6 +219,10 @@ fun dynamicBitwardenColorScheme( weak2 = defaultTheme.status.weak2, error = defaultTheme.status.error, ), + illustration = BitwardenColorScheme.IllustrationColors( + outline = materialColorScheme.tertiaryContainer, + backgroundPrimary = materialColorScheme.onTertiaryContainer, + ), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 953f203b6d..ecadcab29e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -188,7 +188,7 @@ private fun ItemHeaderIcon( modifier = if (iconData is IconData.Local) { modifier.then( Modifier.background( - color = BitwardenTheme.colorScheme.icon.faviconBackground, + color = BitwardenTheme.colorScheme.illustration.backgroundPrimary, shape = BitwardenTheme.shapes.favicon, ), ) @@ -199,7 +199,7 @@ private fun ItemHeaderIcon( BitwardenIcon( iconData = iconData, contentDescription = null, - tint = BitwardenTheme.colorScheme.icon.faviconForeground, + tint = BitwardenTheme.colorScheme.illustration.outline, modifier = Modifier .nullableTestTag(testTag), ) From 537e74389188f870566441427431017dcd1005a5 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 16:50:30 -0500 Subject: [PATCH 03/27] Replaced `standardHorizontalMargin` with explicit horizontal padding --- .../vault/feature/item/component/ItemHeader.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index ecadcab29e..23701cb6dc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -74,8 +74,8 @@ fun ItemHeader( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .padding(horizontal = 16.dp), ) { ItemHeaderIcon( iconData = iconData, @@ -115,8 +115,8 @@ fun ItemHeader( text = stringResource(R.string.no_folder), iconTestTag = "NoFolderIcon", modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .padding(horizontal = 16.dp), ) return@Column } @@ -129,8 +129,8 @@ fun ItemHeader( iconTestTag = "ItemLocationIcon", text = it.name, modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .padding(horizontal = 16.dp), ) } @@ -159,7 +159,7 @@ private fun ExpandingItemLocationContent( text = it.name, iconTestTag = "ItemLocationIcon", modifier = Modifier - .standardHorizontalMargin() + .padding(horizontal = 16.dp) .fillMaxWidth(), ) } @@ -231,7 +231,8 @@ private fun ItemLocationListItem( text = text, style = BitwardenTheme.typography.bodyLarge, color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier.padding(start = 16.dp) + modifier = Modifier + .padding(start = 16.dp) .testTag("ItemLocationText"), ) } From 9e920f1cf5f6a59f8b890e449f1599271a28c2f3 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 16:53:57 -0500 Subject: [PATCH 04/27] Refactor `ItemHeader` to use `cardStyle` and remove custom card styling. --- .../ui/vault/feature/item/component/ItemHeader.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 23701cb6dc..ae10fe9f9d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.platform.testTag @@ -34,8 +33,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +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 @@ -67,9 +66,8 @@ fun ItemHeader( ) { Column( modifier = modifier - .fillMaxWidth() - .clip(shape = BitwardenTheme.shapes.content) - .background(color = BitwardenTheme.colorScheme.background.secondary), + .cardStyle(CardStyle.Full) + .fillMaxWidth(), ) { Row( verticalAlignment = Alignment.CenterVertically, From 0cc7067808c1bb362d7ac204836b5c820801636a Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 16:54:56 -0500 Subject: [PATCH 05/27] Add divider to ItemHeader in vault item view --- .../bitwarden/ui/vault/feature/item/component/ItemHeader.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index ae10fe9f9d..2054241c57 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -15,6 +15,7 @@ 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.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -107,6 +108,8 @@ fun ItemHeader( ) } + HorizontalDivider(Modifier.padding(start = 16.dp, bottom = 8.dp)) + if (relatedLocations.isEmpty()) { ItemLocationListItem( vectorPainter = rememberVectorPainter(R.drawable.ic_folder), From 707312449554a9543887267eec8a48580104c6ef Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 16:55:25 -0500 Subject: [PATCH 06/27] Make `cardStyle` parameter optional in `BitwardenTextField` --- .../ui/platform/components/field/BitwardenTextField.kt | 6 +++--- .../bitwarden/ui/vault/feature/item/component/ItemHeader.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt index aa1cb69152..19be541622 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -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, @@ -206,8 +206,8 @@ fun BitwardenTextField( label: String?, value: String, onValueChange: (String) -> Unit, - supportingContent: (@Composable ColumnScope.() -> Unit)?, - cardStyle: CardStyle, + supportingContent: @Composable (ColumnScope.() -> Unit)?, + cardStyle: CardStyle?, modifier: Modifier = Modifier, tooltip: TooltipData? = null, supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 2054241c57..7ca4c242f8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -103,7 +103,7 @@ fun ItemHeader( ) }, textFieldTestTag = textFieldTestTag, - cardStyle = CardStyle.Full, + cardStyle = null, textStyle = BitwardenTheme.typography.titleMedium, ) } From 6953d5e132eec448f64a71f4e5ce19e9ccdfad4b Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:06:23 -0500 Subject: [PATCH 07/27] Migrate VaultItem related locations to ImmutableList --- .../vault/feature/item/VaultItemViewModel.kt | 7 +- .../feature/item/component/ItemHeader.kt | 23 ++- .../feature/item/model/VaultItemStateData.kt | 3 +- .../feature/item/util/CipherViewExtensions.kt | 3 +- .../vault/feature/item/VaultItemScreenTest.kt | 23 ++- .../feature/item/VaultItemViewModelTest.kt | 175 +++++++++--------- .../item/util/CipherViewExtensionsTest.kt | 29 +-- .../feature/item/util/VaultItemTestUtil.kt | 5 +- 8 files changed, 142 insertions(+), 126 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 8bec1b44f2..e6a3fb62dd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -40,6 +40,8 @@ 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 @@ -160,6 +162,7 @@ class VaultItemViewModel @Inject constructor( addAll(collections.map { VaultItemLocation.Collection(it) }) folderName?.let { add(VaultItemLocation.Folder(it)) } } + .toImmutableList() VaultItemStateData( cipher = cipherView, @@ -1197,7 +1200,7 @@ class VaultItemViewModel @Inject constructor( canEdit = this.data?.canEdit == true, baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled, - relatedLocations = this.data?.relatedLocations.orEmpty(), + relatedLocations = this.data?.relatedLocations.orEmpty().toImmutableList(), ) ?: VaultItemState.ViewState.Error(message = errorText) @@ -1564,7 +1567,7 @@ data class VaultItemState( val canEdit: Boolean, val favorite: Boolean, val iconData: IconData, - val relatedLocations: List, + val relatedLocations: ImmutableList, ) : Parcelable { /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 7ca4c242f8..3e5feae418 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -44,6 +44,9 @@ 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 +import kotlinx.collections.immutable.toImmutableList /** * The max number of items that can be displayed before the "show more" text is visible. @@ -59,7 +62,7 @@ private const val EXPANDABLE_THRESHOLD = 2 fun ItemHeader( value: String, isFavorite: Boolean, - relatedLocations: List, + relatedLocations: ImmutableList, iconData: IconData, modifier: Modifier = Modifier, iconTestTag: String? = null, @@ -136,14 +139,16 @@ fun ItemHeader( } ExpandingItemLocationContent( - overflowLocations = relatedLocations.drop(EXPANDABLE_THRESHOLD), + overflowLocations = relatedLocations + .drop(EXPANDABLE_THRESHOLD) + .toImmutableList(), ) } } @Composable private fun ExpandingItemLocationContent( - overflowLocations: List, + overflowLocations: ImmutableList, ) { var isExpanded by rememberSaveable { mutableStateOf(false) } AnimatedVisibility( @@ -252,7 +257,7 @@ private fun ItemHeader_LocalIcon_Preview() { iconData = IconData.Local( iconRes = R.drawable.ic_globe, ), - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -272,7 +277,7 @@ private fun ItemHeader_NetworkIcon_Preview() { uri = "mockuri", fallbackIconRes = R.drawable.ic_globe, ), - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -291,7 +296,7 @@ private fun ItemHeader_Organization_Preview() { iconData = IconData.Local( iconRes = R.drawable.ic_globe, ), - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization("Stark Industries"), ), ) @@ -312,7 +317,7 @@ private fun ItemNameField_Org_SingleCollection_Preview() { iconData = IconData.Local( iconRes = R.drawable.ic_globe, ), - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization("Stark Industries"), VaultItemLocation.Collection("Marketing"), ), @@ -334,7 +339,7 @@ private fun ItemNameField_Org_MultiCollection_Preview() { iconData = IconData.Local( iconRes = R.drawable.ic_globe, ), - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization("Stark Industries"), VaultItemLocation.Collection("Marketing"), VaultItemLocation.Collection("Product"), @@ -357,7 +362,7 @@ private fun ItemNameField_Org_SingleCollection_Folder_Preview() { iconData = IconData.Local( iconRes = R.drawable.ic_note, ), - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization("Stark Industries"), VaultItemLocation.Collection("Marketing"), VaultItemLocation.Folder("Competition"), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt index bb61678f6e..862e81c1b0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt @@ -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. @@ -18,5 +19,5 @@ data class VaultItemStateData( val canDelete: Boolean, val canAssociateToCollections: Boolean, val canEdit: Boolean, - val relatedLocations: List, + val relatedLocations: ImmutableList, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index 0703857f85..482295f976 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -27,6 +27,7 @@ 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" @@ -48,7 +49,7 @@ fun CipherView.toViewState( canEdit: Boolean, baseIconUrl: String, isIconLoadingDisabled: Boolean, - relatedLocations: List, + relatedLocations: ImmutableList, ): VaultItemState.ViewState = VaultItemState.ViewState.Content( common = VaultItemState.ViewState.Content.Common( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 9078d17685..934a579f95 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -53,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 @@ -324,7 +325,7 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = defaultViewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ), ), ) @@ -338,7 +339,9 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = defaultViewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = listOf(VaultItemLocation.Collection("collection")), + relatedLocations = persistentListOf( + VaultItemLocation.Collection("collection"), + ), ), ), ) @@ -357,7 +360,7 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = viewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization( organizationName, ), @@ -374,7 +377,7 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = viewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ), ), ) @@ -392,7 +395,7 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = viewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Collection("My collection"), ), ), @@ -407,7 +410,7 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = viewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ), ), ) @@ -425,7 +428,7 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = viewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization("My organization"), VaultItemLocation.Collection("My collection"), VaultItemLocation.Folder("My folder"), @@ -452,7 +455,7 @@ class VaultItemScreenTest : BaseComposeTest() { DEFAULT_STATE.copy( viewState = viewState.copy( common = DEFAULT_COMMON.copy( - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization("My organization"), VaultItemLocation.Collection("My collection"), VaultItemLocation.Folder("My folder"), @@ -2948,7 +2951,7 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = canEdit = true, favorite = false, iconData = IconData.Local(iconRes = R.drawable.ic_globe), - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) private val DEFAULT_PASSKEY = R.string.created_xy.asText( @@ -3035,7 +3038,7 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common = canEdit = true, favorite = false, iconData = IconData.Local(iconRes = R.drawable.ic_globe), - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 560a6f1ece..816324e229 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -56,6 +56,7 @@ import io.mockk.mockkStatic import io.mockk.runs import io.mockk.unmockkStatic import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach @@ -201,7 +202,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -231,7 +232,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -254,7 +255,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginState @@ -303,7 +304,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginState @@ -341,7 +342,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -390,7 +391,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -442,7 +443,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -486,7 +487,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -525,7 +526,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns viewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -568,7 +569,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -614,7 +615,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -669,7 +670,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -707,7 +708,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -730,7 +731,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } assertEquals( @@ -762,7 +763,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState @@ -831,7 +832,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState @@ -891,7 +892,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState @@ -962,7 +963,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -992,7 +993,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -1012,7 +1013,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) every { clipboardManager.setText(text = field) } just runs @@ -1036,7 +1037,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) organizationEventManager.trackEvent( event = OrganizationEvent.CipherClientCopiedHiddenField( @@ -1080,7 +1081,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1118,7 +1119,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -1156,7 +1157,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1194,7 +1195,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) organizationEventManager.trackEvent( event = OrganizationEvent.CipherClientToggledHiddenFieldVisible( @@ -1219,7 +1220,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1249,7 +1250,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -1273,7 +1274,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1313,7 +1314,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1345,7 +1346,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -1373,7 +1374,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1427,7 +1428,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1457,7 +1458,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -1481,7 +1482,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1519,7 +1520,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1549,7 +1550,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -1573,7 +1574,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1621,7 +1622,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1683,7 +1684,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1754,7 +1755,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = null, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1926,7 +1927,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE @@ -1972,7 +1973,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = createTotpCodeData(), baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2018,7 +2019,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = createTotpCodeData(), baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } coVerify(exactly = 1) { @@ -2041,7 +2042,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = createTotpCodeData(), baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2074,7 +2075,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = createTotpCodeData(), baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2094,7 +2095,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = createTotpCodeData(), baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2120,7 +2121,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = createTotpCodeData(), baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2176,7 +2177,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { totpCodeItemData = createTotpCodeData(), baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2202,7 +2203,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2246,7 +2247,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2277,7 +2278,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2297,7 +2298,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } .returns( @@ -2330,7 +2331,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2351,7 +2352,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2386,7 +2387,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2410,7 +2411,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2448,7 +2449,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) organizationEventManager.trackEvent( event = OrganizationEvent.CipherClientToggledPasswordVisible( @@ -2487,7 +2488,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2519,7 +2520,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2539,7 +2540,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2567,7 +2568,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2587,7 +2588,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2619,7 +2620,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2639,7 +2640,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2670,7 +2671,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2690,7 +2691,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2722,7 +2723,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2742,7 +2743,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2770,7 +2771,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2790,7 +2791,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2822,7 +2823,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2841,7 +2842,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2872,7 +2873,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2904,7 +2905,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2942,7 +2943,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns sshKeyViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -2976,7 +2977,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -2997,7 +2998,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -3030,7 +3031,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } } @@ -3050,7 +3051,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -3086,7 +3087,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -3127,7 +3128,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -3168,7 +3169,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns IDENTITY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -3323,7 +3324,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = listOf( + relatedLocations = persistentListOf( VaultItemLocation.Organization("mockOrganizationName"), VaultItemLocation.Collection("mockName-1"), VaultItemLocation.Folder("mockName-1"), @@ -3394,7 +3395,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns viewState val viewModel = createViewModel(state = null) @@ -3439,7 +3440,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns viewState val viewModel = createViewModel(state = null) @@ -3479,7 +3480,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, isIconLoadingDisabled = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) } returns viewState val viewModel = createViewModel(state = null) @@ -3736,7 +3737,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { canEdit = true, favorite = false, iconData = IconData.Local(R.drawable.ic_globe), - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) private val DEFAULT_VIEW_STATE: VaultItemState.ViewState.Content = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt index 0cc6e3e401..b9fe440e48 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt @@ -10,6 +10,7 @@ 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 @@ -70,7 +71,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -111,7 +112,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -145,7 +146,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -179,7 +180,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -219,7 +220,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -249,7 +250,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -277,7 +278,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -308,7 +309,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -349,7 +350,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -395,7 +396,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -443,7 +444,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( @@ -475,7 +476,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) val expectedState = VaultItemState.ViewState.Content( @@ -505,7 +506,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( VaultItemState.ViewState.Content( @@ -552,7 +553,7 @@ class CipherViewExtensionsTest { canEdit = true, baseIconUrl = "https://example.com/", isIconLoadingDisabled = true, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), ) assertEquals( it.value, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index 57b99e2af7..12c04c6643 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -19,6 +19,7 @@ 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" @@ -177,7 +178,7 @@ fun createCommonContent( canAssignToCollections = true, canEdit = true, favorite = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), iconData = IconData.Local(iconResId), ) } else { @@ -226,7 +227,7 @@ fun createCommonContent( canAssignToCollections = true, canEdit = true, favorite = false, - relatedLocations = emptyList(), + relatedLocations = persistentListOf(), iconData = IconData.Local(iconResId), ) } From 78d59652715261c036d5b802ee4ec2bd8d93fb12 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:12:41 -0500 Subject: [PATCH 08/27] Refactor `ItemHeader` to use `LazyColumn` for overflow locations --- .../feature/item/component/ItemHeader.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 3e5feae418..b432946009 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -15,6 +15,7 @@ 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.items import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -157,18 +158,17 @@ private fun ExpandingItemLocationContent( exit = fadeOut() + slideOutVertically(), modifier = Modifier.clipToBounds(), ) { - Column { - overflowLocations - .forEach { - ItemLocationListItem( - vectorPainter = rememberVectorPainter(it.icon), - text = it.name, - iconTestTag = "ItemLocationIcon", - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - } + LazyColumn { + items(overflowLocations) { + ItemLocationListItem( + vectorPainter = rememberVectorPainter(it.icon), + text = it.name, + iconTestTag = "ItemLocationIcon", + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + } } } From 6da8e2c47be4469bda1a2fc220bb463884b8a167 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:18:44 -0500 Subject: [PATCH 09/27] Adjust height of Spacer in `BitwardenTextField` based on `cardStyle` presence --- .../ui/platform/components/field/BitwardenTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt index 19be541622..77409e3b0d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -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 -> From aba8344df105f2d67bc243b01f235d9a67a8c323 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:20:37 -0500 Subject: [PATCH 10/27] Revert unintentional change --- .../ui/platform/components/field/BitwardenTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt index 77409e3b0d..51007fa6c8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -206,7 +206,7 @@ fun BitwardenTextField( label: String?, value: String, onValueChange: (String) -> Unit, - supportingContent: @Composable (ColumnScope.() -> Unit)?, + supportingContent: (@Composable ColumnScope.() -> Unit)?, cardStyle: CardStyle?, modifier: Modifier = Modifier, tooltip: TooltipData? = null, From 4f49d3d5041ee5b396cb92204cb29e1e472b7a8d Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:23:33 -0500 Subject: [PATCH 11/27] Reduce `Spacer` height in `VaultItemLoginContent.kt` from 24.dp to 12.dp --- .../bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 51a5f27325..02cb83d7d4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -54,7 +54,7 @@ fun VaultItemLoginContent( modifier = modifier, ) { item { - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(12.dp)) ItemHeader( value = commonState.name, isFavorite = commonState.favorite, From 234f49a92ce48c1c3e2012e41d06f79f378c10b1 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:37:42 -0500 Subject: [PATCH 12/27] Replace `HorizontalDivider` with `BitwardenHorizontalDivider` and add `Spacer` in `ItemHeader.kt` --- .../ui/vault/feature/item/component/ItemHeader.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index b432946009..43c125afc5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -11,12 +11,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -37,6 +38,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.cardStyle import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider 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 @@ -112,7 +114,9 @@ fun ItemHeader( ) } - HorizontalDivider(Modifier.padding(start = 16.dp, bottom = 8.dp)) + BitwardenHorizontalDivider(Modifier.padding(start = 16.dp)) + + Spacer(Modifier.height(8.dp)) if (relatedLocations.isEmpty()) { ItemLocationListItem( From 727d943fae14c247b26cefad556ce43dae0baca9 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:43:53 -0500 Subject: [PATCH 13/27] Use `persistentListOfNotNull` instead of `buildList` and `toImmutableList` --- .../ui/vault/feature/item/VaultItemViewModel.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index e6a3fb62dd..7d6fbbabcd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -157,12 +157,11 @@ class VaultItemViewModel @Inject constructor( folderState.data?.firstOrNull { folder -> folderId == folder.id } } ?.name - val relatedLocations = buildList { - organizationName?.let { add(VaultItemLocation.Organization(it)) } - addAll(collections.map { VaultItemLocation.Collection(it) }) - folderName?.let { add(VaultItemLocation.Folder(it)) } - } - .toImmutableList() + val relatedLocations = persistentListOfNotNull( + organizationName?.let { VaultItemLocation.Organization(it) }, + *collections.map { VaultItemLocation.Collection(it) }.toTypedArray(), + folderName?.let { VaultItemLocation.Folder(it) }, + ) VaultItemStateData( cipher = cipherView, From 0ba240852f47ed03151e61b720628ede06b8691e Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 24 Feb 2025 17:46:22 -0500 Subject: [PATCH 14/27] Refactor `ExpandingItemLocationContent` to use `ColumnScope` --- .../bitwarden/ui/vault/feature/item/component/ItemHeader.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 43c125afc5..03cc7839d0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -152,7 +153,7 @@ fun ItemHeader( } @Composable -private fun ExpandingItemLocationContent( +private fun ColumnScope.ExpandingItemLocationContent( overflowLocations: ImmutableList, ) { var isExpanded by rememberSaveable { mutableStateOf(false) } From d7d099477ffa3df84e09b006f56c3383b6def04e Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 25 Feb 2025 17:35:02 -0500 Subject: [PATCH 15/27] Refactor `ItemHeader` to use `LazyColumn` and `Crossfade` for smoother transitions - Migrates `ItemHeader` to `LazyColumn` to improve performance. - Introduces `Crossfade` for animating title changes in `BitwardenExpandingHeader`. - Adjusts icon sizing in `ItemHeaderIcon`. - Removes unnecessary column scope and animated visibility from `ExpandingItemLocationContent`. - Refactors to use `LazyItemScope` and adds `animateItem()` to `ItemLocationListItem`. - Adds conditional handling for expanding the item locations list. --- .../header/BitwardenExpandingHeader.kt | 60 +- .../feature/item/VaultItemCardContent.kt | 460 ++++++++------- .../feature/item/VaultItemIdentityContent.kt | 558 +++++++++--------- .../feature/item/VaultItemLoginContent.kt | 442 +++++++------- .../item/VaultItemSecureNoteContent.kt | 220 +++---- .../feature/item/VaultItemSshKeyContent.kt | 339 +++++------ .../feature/item/component/ItemHeader.kt | 343 +++++------ 7 files changed, 1193 insertions(+), 1229 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt index b07e80f5ce..c2f78bc84e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt @@ -1,6 +1,9 @@ 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.Row import androidx.compose.foundation.layout.padding @@ -50,23 +53,46 @@ fun BitwardenExpandingHeader( .semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = if (isExpanded) expandedText else 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), - ) + 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), + ) + } } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index ab08298838..b0bcdd3b1b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.item +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -41,253 +42,254 @@ fun VaultItemCardContent( vaultCardItemTypeHandlers: VaultCardItemTypeHandlers, modifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier) { - item { - Spacer(modifier = Modifier.height(height = 12.dp)) - ItemHeader( - value = commonState.name, - isFavorite = commonState.favorite, - iconData = commonState.iconData, - relatedLocations = commonState.relatedLocations, - iconTestTag = "CardItemNameIcon", - textFieldTestTag = "CardItemNameEntry", - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - cardState.cardholderName?.let { cardholderName -> - item { - BitwardenTextField( - label = stringResource(id = R.string.cardholder_name), - value = cardholderName, - onValueChange = {}, - readOnly = true, - singleLine = false, - textFieldTestTag = "CardholderNameEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = cardholderName), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + Column(modifier = modifier) { + Spacer(Modifier.height(height = 12.dp)) + ItemHeader( + value = commonState.name, + isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "CardItemNameIcon", + textFieldTestTag = "CardItemNameEntry", + ) + LazyColumn(modifier = modifier) { + cardState.cardholderName?.let { cardholderName -> + item { + BitwardenTextField( + label = stringResource(id = R.string.cardholder_name), + value = cardholderName, + onValueChange = {}, + readOnly = true, + singleLine = false, + textFieldTestTag = "CardholderNameEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = cardholderName), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - cardState.number?.let { numberData -> - item { - BitwardenPasswordField( - label = stringResource(id = R.string.number), - value = numberData.number, - onValueChange = {}, - showPassword = numberData.isVisible, - showPasswordChange = vaultCardItemTypeHandlers.onShowNumberClick, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_number), - onClick = vaultCardItemTypeHandlers.onCopyNumberClick, - modifier = Modifier.testTag(tag = "CardCopyNumberButton"), - ) - }, - passwordFieldTestTag = "CardNumberEntry", - showPasswordTestTag = "CardViewNumberButton", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = numberData), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + cardState.number?.let { numberData -> + item { + BitwardenPasswordField( + label = stringResource(id = R.string.number), + value = numberData.number, + onValueChange = {}, + showPassword = numberData.isVisible, + showPasswordChange = vaultCardItemTypeHandlers.onShowNumberClick, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_number), + onClick = vaultCardItemTypeHandlers.onCopyNumberClick, + modifier = Modifier.testTag(tag = "CardCopyNumberButton"), + ) + }, + passwordFieldTestTag = "CardNumberEntry", + showPasswordTestTag = "CardViewNumberButton", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = numberData), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) { - item { - BitwardenTextField( - label = stringResource(id = R.string.brand), - value = cardState.brand.shortName(), - onValueChange = {}, - readOnly = true, - singleLine = false, - textFieldTestTag = "CardBrandEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = cardState.brand), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) { + item { + BitwardenTextField( + label = stringResource(id = R.string.brand), + value = cardState.brand.shortName(), + onValueChange = {}, + readOnly = true, + singleLine = false, + textFieldTestTag = "CardBrandEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = cardState.brand), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - cardState.expiration?.let { expiration -> - item { - BitwardenTextField( - label = stringResource(id = R.string.expiration), - value = expiration, - onValueChange = {}, - readOnly = true, - singleLine = false, - textFieldTestTag = "CardExpirationEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = expiration), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + cardState.expiration?.let { expiration -> + item { + BitwardenTextField( + label = stringResource(id = R.string.expiration), + value = expiration, + onValueChange = {}, + readOnly = true, + singleLine = false, + textFieldTestTag = "CardExpirationEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = expiration), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - cardState.securityCode?.let { securityCodeData -> - item { - BitwardenPasswordField( - label = stringResource(id = R.string.security_code), - value = securityCodeData.code, - onValueChange = {}, - showPassword = securityCodeData.isVisible, - showPasswordChange = vaultCardItemTypeHandlers.onShowSecurityCodeClick, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_security_code), - onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick, - modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"), - ) - }, - showPasswordTestTag = "CardViewSecurityCodeButton", - passwordFieldTestTag = "CardSecurityCodeEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = securityCodeData), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + cardState.securityCode?.let { securityCodeData -> + item { + BitwardenPasswordField( + label = stringResource(id = R.string.security_code), + value = securityCodeData.code, + onValueChange = {}, + showPassword = securityCodeData.isVisible, + showPasswordChange = vaultCardItemTypeHandlers.onShowSecurityCodeClick, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource( + id = R.string.copy_security_code, + ), + onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick, + modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"), + ) + }, + showPasswordTestTag = "CardViewSecurityCodeButton", + passwordFieldTestTag = "CardSecurityCodeEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = securityCodeData), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .testTag("CipherAttachment") + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } } - } - commonState.notes?.let { notes -> item { Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), + .padding(horizontal = 12.dp), ) } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } - - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_updated)}: ", - text = commonState.lastUpdated, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp), - ) - } - item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index 5aca1a9bed..cabc7f601f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.item +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -38,303 +39,302 @@ fun VaultItemIdentityContent( vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers, modifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier) { - item { - Spacer(modifier = Modifier.height(height = 12.dp)) - ItemHeader( - value = commonState.name, - isFavorite = commonState.favorite, - iconData = commonState.iconData, - relatedLocations = commonState.relatedLocations, - iconTestTag = "IdentityItemNameIcon", - textFieldTestTag = "IdentityItemNameEntry", - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - identityState.identityName?.let { identityName -> - item { - IdentityCopyField( - label = stringResource(id = R.string.identity_name), - value = identityName, - copyContentDescription = stringResource(id = R.string.copy_identity_name), - textFieldTestTag = "IdentityNameEntry", - copyActionTestTag = "IdentityCopyNameButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = identityName), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + Column(modifier = modifier) { + Spacer(Modifier.height(height = 12.dp)) + ItemHeader( + value = commonState.name, + isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "IdentityItemNameIcon", + textFieldTestTag = "IdentityItemNameEntry", + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + LazyColumn { + identityState.identityName?.let { identityName -> + item { + IdentityCopyField( + label = stringResource(id = R.string.identity_name), + value = identityName, + copyContentDescription = stringResource(id = R.string.copy_identity_name), + textFieldTestTag = "IdentityNameEntry", + copyActionTestTag = "IdentityCopyNameButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = identityName), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.username?.let { username -> - item { - IdentityCopyField( - label = stringResource(id = R.string.username), - value = username, - copyContentDescription = stringResource(id = R.string.copy_username), - textFieldTestTag = "IdentityUsernameEntry", - copyActionTestTag = "IdentityCopyUsernameButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = username), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.username?.let { username -> + item { + IdentityCopyField( + label = stringResource(id = R.string.username), + value = username, + copyContentDescription = stringResource(id = R.string.copy_username), + textFieldTestTag = "IdentityUsernameEntry", + copyActionTestTag = "IdentityCopyUsernameButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = username), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.company?.let { company -> - item { - IdentityCopyField( - label = stringResource(id = R.string.company), - value = company, - copyContentDescription = stringResource(id = R.string.copy_company), - textFieldTestTag = "IdentityCompanyEntry", - copyActionTestTag = "IdentityCopyCompanyButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = company), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.company?.let { company -> + item { + IdentityCopyField( + label = stringResource(id = R.string.company), + value = company, + copyContentDescription = stringResource(id = R.string.copy_company), + textFieldTestTag = "IdentityCompanyEntry", + copyActionTestTag = "IdentityCopyCompanyButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = company), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.ssn?.let { ssn -> - item { - IdentityCopyField( - label = stringResource(id = R.string.ssn), - value = ssn, - copyContentDescription = stringResource(id = R.string.copy_ssn), - textFieldTestTag = "IdentitySsnEntry", - copyActionTestTag = "IdentityCopySsnButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = ssn), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.ssn?.let { ssn -> + item { + IdentityCopyField( + label = stringResource(id = R.string.ssn), + value = ssn, + copyContentDescription = stringResource(id = R.string.copy_ssn), + textFieldTestTag = "IdentitySsnEntry", + copyActionTestTag = "IdentityCopySsnButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = ssn), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.passportNumber?.let { passportNumber -> - item { - IdentityCopyField( - label = stringResource(id = R.string.passport_number), - value = passportNumber, - copyContentDescription = stringResource(id = R.string.copy_passport_number), - textFieldTestTag = "IdentityPassportNumberEntry", - copyActionTestTag = "IdentityCopyPassportNumberButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = passportNumber), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.passportNumber?.let { passportNumber -> + item { + IdentityCopyField( + label = stringResource(id = R.string.passport_number), + value = passportNumber, + copyContentDescription = stringResource(id = R.string.copy_passport_number), + textFieldTestTag = "IdentityPassportNumberEntry", + copyActionTestTag = "IdentityCopyPassportNumberButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = passportNumber), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.licenseNumber?.let { licenseNumber -> - item { - IdentityCopyField( - label = stringResource(id = R.string.license_number), - value = licenseNumber, - copyContentDescription = stringResource(id = R.string.copy_license_number), - textFieldTestTag = "IdentityLicenseNumberEntry", - copyActionTestTag = "IdentityCopyLicenseNumberButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = licenseNumber), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.licenseNumber?.let { licenseNumber -> + item { + IdentityCopyField( + label = stringResource(id = R.string.license_number), + value = licenseNumber, + copyContentDescription = stringResource(id = R.string.copy_license_number), + textFieldTestTag = "IdentityLicenseNumberEntry", + copyActionTestTag = "IdentityCopyLicenseNumberButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = licenseNumber), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.email?.let { email -> - item { - IdentityCopyField( - label = stringResource(id = R.string.email), - value = email, - copyContentDescription = stringResource(id = R.string.copy_email), - textFieldTestTag = "IdentityEmailEntry", - copyActionTestTag = "IdentityCopyEmailButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = email), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.email?.let { email -> + item { + IdentityCopyField( + label = stringResource(id = R.string.email), + value = email, + copyContentDescription = stringResource(id = R.string.copy_email), + textFieldTestTag = "IdentityEmailEntry", + copyActionTestTag = "IdentityCopyEmailButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = email), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.phone?.let { phone -> - item { - IdentityCopyField( - label = stringResource(id = R.string.phone), - value = phone, - copyContentDescription = stringResource(id = R.string.copy_phone), - textFieldTestTag = "IdentityPhoneEntry", - copyActionTestTag = "IdentityCopyPhoneButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = phone), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.phone?.let { phone -> + item { + IdentityCopyField( + label = stringResource(id = R.string.phone), + value = phone, + copyContentDescription = stringResource(id = R.string.copy_phone), + textFieldTestTag = "IdentityPhoneEntry", + copyActionTestTag = "IdentityCopyPhoneButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = phone), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - identityState.address?.let { address -> - item { - IdentityCopyField( - label = stringResource(id = R.string.address), - value = address, - copyContentDescription = stringResource(id = R.string.copy_address), - textFieldTestTag = "IdentityAddressEntry", - copyActionTestTag = "IdentityCopyAddressButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = address), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + identityState.address?.let { address -> + item { + IdentityCopyField( + label = stringResource(id = R.string.address), + value = address, + copyContentDescription = stringResource(id = R.string.copy_address), + textFieldTestTag = "IdentityAddressEntry", + copyActionTestTag = "IdentityCopyAddressButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = address), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } - } - commonState.notes?.let { notes -> + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + IdentityCopyField( + label = stringResource(id = R.string.notes), + value = notes, + copyContentDescription = stringResource(id = R.string.copy_notes), + textFieldTestTag = "CipherNotesLabel", + copyActionTestTag = "CipherNotesCopyButton", + onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .testTag("CipherAttachment") + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } + } + item { Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - IdentityCopyField( - label = stringResource(id = R.string.notes), - value = notes, - copyContentDescription = stringResource(id = R.string.copy_notes), - textFieldTestTag = "CipherNotesLabel", - copyActionTestTag = "CipherNotesCopyButton", - onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), + .padding(horizontal = 12.dp), ) } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } - - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_updated)}: ", - text = commonState.lastUpdated, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp), - ) - } - item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 02cb83d7d4..89ea43d819 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.item +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -50,246 +51,229 @@ fun VaultItemLoginContent( vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers, modifier: Modifier = Modifier, ) { - LazyColumn( - modifier = modifier, + Column( + modifier = modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) { - item { - Spacer(Modifier.height(12.dp)) - ItemHeader( - value = commonState.name, - isFavorite = commonState.favorite, - iconData = commonState.iconData, - relatedLocations = commonState.relatedLocations, - iconTestTag = "LoginItemNameIcon", - textFieldTestTag = "LoginItemNameEntry", - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + Spacer(Modifier.height(height = 12.dp)) + ItemHeader( + value = commonState.name, + isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "LoginItemNameIcon", + textFieldTestTag = "LoginItemNameEntry", + ) + LazyColumn( + modifier = modifier, + ) { + if (loginItemState.hasLoginCredentials) { + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.login_credentials), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + } + + loginItemState.username?.let { username -> + item { + UsernameField( + username = username, + onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick, + cardStyle = loginItemState + .passwordData + ?.let { CardStyle.Top(dividerPadding = 0.dp) } + ?: CardStyle.Full, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + + loginItemState.passwordData?.let { passwordData -> + item { + PasswordField( + passwordData = passwordData, + onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick, + onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick, + onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick, + cardStyle = loginItemState + .username + ?.let { CardStyle.Bottom } + ?: CardStyle.Full, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + + loginItemState.fido2CredentialCreationDateText?.let { creationDate -> + item { + Spacer(modifier = Modifier.height(8.dp)) + Fido2CredentialField( + creationDate = creationDate(), + modifier = Modifier + .fillMaxWidth(), + ) + } + } + + loginItemState.totpCodeItemData?.let { totpCodeItemData -> + item { + Spacer(modifier = Modifier.height(8.dp)) + TotpField( + totpCodeItemData = totpCodeItemData, + enabled = loginItemState.canViewTotpCode, + onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick, + onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers + .onAuthenticatorHelpToolTipClick, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + + loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.autofill_options), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + + itemsIndexed(uris) { index, uriData -> + UriField( + uriData = uriData, + onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick, + onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick, + cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp), + modifier = Modifier + .fillMaxWidth(), + ) + } + } + + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), + modifier = Modifier + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + NotesField( + notes = notes, + onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth(), + ) + } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .fillMaxWidth(), + attachmentItem = attachmentItem, + cardStyle = attachments.toListItemCardStyle(index = index), + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + ) + } + } - if (loginItemState.hasLoginCredentials) { item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.login_credentials), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - } - - loginItemState.username?.let { username -> - item { - UsernameField( - username = username, - onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick, - cardStyle = loginItemState - .passwordData - ?.let { CardStyle.Top(dividerPadding = 0.dp) } - ?: CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - loginItemState.passwordData?.let { passwordData -> - item { - PasswordField( - passwordData = passwordData, - onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick, - onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick, - onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick, - cardStyle = loginItemState - .username - ?.let { CardStyle.Bottom } - ?: CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - loginItemState.fido2CredentialCreationDateText?.let { creationDate -> - item { - Spacer(modifier = Modifier.height(8.dp)) - Fido2CredentialField( - creationDate = creationDate(), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - loginItemState.totpCodeItemData?.let { totpCodeItemData -> - item { - Spacer(modifier = Modifier.height(8.dp)) - TotpField( - totpCodeItemData = totpCodeItemData, - enabled = loginItemState.canViewTotpCode, - onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick, - onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers - .onAuthenticatorHelpToolTipClick, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.autofill_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - - itemsIndexed(uris) { index, uriData -> - UriField( - uriData = uriData, - onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick, - onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick, - cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.notes?.let { notes -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - NotesField( - notes = notes, - onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - cardStyle = attachments.toListItemCardStyle(index = index), - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - ) - } - } - - item { - 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), - ) - } - - loginItemState.passwordRevisionDate?.let { revisionDate -> - item { - Spacer(modifier = Modifier.height(height = 4.dp)) + Spacer(modifier = Modifier.height(16.dp)) VaultItemUpdateText( - header = "${stringResource(id = R.string.date_password_updated)}: ", - text = revisionDate, + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp), + .fillMaxWidth(), ) } - } - loginItemState.passwordHistoryCount?.let { passwordHistoryCount -> + loginItemState.passwordRevisionDate?.let { revisionDate -> + item { + Spacer(modifier = Modifier.height(height = 4.dp)) + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_password_updated)}: ", + text = revisionDate, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + ) + } + } + + loginItemState.passwordHistoryCount?.let { passwordHistoryCount -> + item { + Spacer(modifier = Modifier.height(height = 4.dp)) + BitwardenHyperTextLink( + annotatedResId = R.string.password_history_count, + args = arrayOf(passwordHistoryCount.toString()), + annotationKey = "passwordHistory", + accessibilityString = stringResource(id = R.string.password_history), + onClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick, + style = BitwardenTheme.typography.labelMedium, + modifier = Modifier + .wrapContentWidth() + .padding(horizontal = 12.dp), + ) + } + } + item { - Spacer(modifier = Modifier.height(height = 4.dp)) - BitwardenHyperTextLink( - annotatedResId = R.string.password_history_count, - args = arrayOf(passwordHistoryCount.toString()), - annotationKey = "passwordHistory", - accessibilityString = stringResource(id = R.string.password_history), - onClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier - .wrapContentWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp), - ) + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } - - item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) - } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index ed420e4293..3655c59aa7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.item +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -38,126 +39,129 @@ fun VaultItemSecureNoteContent( vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, modifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier) { - item { - Spacer(modifier = Modifier.height(height = 12.dp)) - ItemHeader( - value = commonState.name, - isFavorite = commonState.favorite, - iconData = commonState.iconData, - relatedLocations = commonState.relatedLocations, - iconTestTag = "SecureNoteItemNameIcon", - textFieldTestTag = "SecureNoteItemNameEntry", - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + Column(modifier = modifier) { + Spacer(Modifier.height(height = 12.dp)) + ItemHeader( + value = commonState.name, + isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "SecureNoteItemNameIcon", + textFieldTestTag = "SecureNoteItemNameEntry", + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + LazyColumn { - commonState.notes?.let { notes -> - item { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + } + + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .testTag("CipherAttachment") + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } } - } - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), + Row( modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) + .padding(horizontal = 12.dp) + .semantics(mergeDescendants = true) { }, + ) { + Text( + text = "${stringResource(id = R.string.date_updated)}: ", + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.primary, + ) + Text( + text = commonState.lastUpdated, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.primary, + ) + } } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } - - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .semantics(mergeDescendants = true) { }, - ) { - Text( - text = "${stringResource(id = R.string.date_updated)}: ", - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - Text( - text = commonState.lastUpdated, - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - } - } - - item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt index 5536345183..5cd6a633ec 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.item +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -39,207 +40,195 @@ fun VaultItemSshKeyContent( vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, modifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier) { - 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)) - } - - item { - ItemHeader( - value = commonState.name, - isFavorite = commonState.favorite, - iconData = commonState.iconData, - relatedLocations = commonState.relatedLocations, - iconTestTag = "SshKeyItemNameIcon", - textFieldTestTag = "SshKeyItemNameEntry", - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - - item { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.public_key), - value = sshKeyItemState.publicKey, - onValueChange = { }, - singleLine = false, - readOnly = true, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_public_key), - onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick, - modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"), - ) - }, - cardStyle = CardStyle.Top(), - modifier = Modifier - .testTag("SshKeyItemPublicKeyEntry") - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - - item { - BitwardenPasswordField( - label = stringResource(id = R.string.private_key), - value = sshKeyItemState.privateKey, - onValueChange = { }, - singleLine = false, - readOnly = true, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_private_key), - onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick, - modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"), - ) - }, - showPassword = sshKeyItemState.showPrivateKey, - showPasswordTestTag = "ViewPrivateKeyButton", - showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick, - cardStyle = CardStyle.Middle(), - modifier = Modifier - .testTag("SshKeyItemPrivateKeyEntry") - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - - item { - BitwardenTextField( - label = stringResource(id = R.string.fingerprint), - value = sshKeyItemState.fingerprint, - onValueChange = { }, - singleLine = false, - readOnly = true, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_fingerprint), - onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick, - modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"), - ) - }, - cardStyle = CardStyle.Bottom, - modifier = Modifier - .testTag("SshKeyItemFingerprintEntry") - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - - commonState.notes?.let { notes -> + Column(modifier = modifier) { + Spacer(Modifier.height(height = 12.dp)) + ItemHeader( + value = commonState.name, + isFavorite = commonState.favorite, + iconData = commonState.iconData, + relatedLocations = commonState.relatedLocations, + iconTestTag = "SshKeyItemNameIcon", + textFieldTestTag = "SshKeyItemNameEntry", + ) + LazyColumn(modifier = modifier) { item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( - label = stringResource(id = R.string.notes), - value = notes, + label = stringResource(id = R.string.public_key), + value = sshKeyItemState.publicKey, onValueChange = { }, - readOnly = true, singleLine = false, + readOnly = true, actions = { BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + contentDescription = stringResource(id = R.string.copy_public_key), + onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick, + modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"), ) }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, + cardStyle = CardStyle.Top(), modifier = Modifier + .testTag("SshKeyItemPublicKeyEntry") .fillMaxWidth() .standardHorizontalMargin(), ) } - } - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + BitwardenPasswordField( + label = stringResource(id = R.string.private_key), + value = sshKeyItemState.privateKey, + onValueChange = { }, + singleLine = false, + readOnly = true, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_private_key), + onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick, + modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"), + ) + }, + showPassword = sshKeyItemState.showPrivateKey, + showPasswordTestTag = "ViewPrivateKeyButton", + showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick, + cardStyle = CardStyle.Middle(), + modifier = Modifier + .testTag("SshKeyItemPrivateKeyEntry") + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + item { + BitwardenTextField( + label = stringResource(id = R.string.fingerprint), + value = sshKeyItemState.fingerprint, + onValueChange = { }, + singleLine = false, + readOnly = true, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_fingerprint), + onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick, + modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"), + ) + }, + cardStyle = CardStyle.Bottom, + modifier = Modifier + .testTag("SshKeyItemFingerprintEntry") + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } + } + item { Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 16.dp), + .padding(horizontal = 12.dp) + .testTag("SshKeyItemLastUpdated"), ) } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } - - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_updated)}: ", - text = commonState.lastUpdated, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .testTag("SshKeyItemLastUpdated"), - ) - } - - item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 03cc7839d0..0d6368fcde 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -1,23 +1,18 @@ package com.x8bit.bitwarden.ui.vault.feature.item.component import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.items import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -28,7 +23,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -36,7 +30,6 @@ 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.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.cardStyle import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider @@ -50,7 +43,6 @@ 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 -import kotlinx.collections.immutable.toImmutableList /** * The max number of items that can be displayed before the "show more" text is visible. @@ -58,9 +50,8 @@ import kotlinx.collections.immutable.toImmutableList private const val EXPANDABLE_THRESHOLD = 2 /** - * Reusable composable for displaying the cipher name and favorite status. + * Reusable composable for displaying the cipher name, favorite status, and related locations. */ -@OmitFromCoverage @Suppress("LongMethod") @Composable fun ItemHeader( @@ -72,119 +63,107 @@ fun ItemHeader( iconTestTag: String? = null, textFieldTestTag: String? = null, ) { - Column( + var isExpanded by rememberSaveable { mutableStateOf(false) } + LazyColumn( modifier = modifier .cardStyle(CardStyle.Full) .fillMaxWidth(), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 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, - ) - } - - BitwardenHorizontalDivider(Modifier.padding(start = 16.dp)) - - Spacer(Modifier.height(8.dp)) - - if (relatedLocations.isEmpty()) { - ItemLocationListItem( - vectorPainter = rememberVectorPainter(R.drawable.ic_folder), - text = stringResource(R.string.no_folder), - iconTestTag = "NoFolderIcon", + item { + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - ) - return@Column + ) { + 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, + ) + } + BitwardenHorizontalDivider(Modifier.padding(start = 16.dp)) + + Spacer(Modifier.height(8.dp)) } - relatedLocations - .take(EXPANDABLE_THRESHOLD) - .forEach { + if (relatedLocations.isEmpty()) { + item { ItemLocationListItem( - vectorPainter = rememberVectorPainter(it.icon), - iconTestTag = "ItemLocationIcon", - text = it.name, + vectorPainter = rememberVectorPainter(R.drawable.ic_folder), + text = stringResource(R.string.no_folder), + iconTestTag = "NoFolderIcon", modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) } + return@LazyColumn + } - ExpandingItemLocationContent( - overflowLocations = relatedLocations - .drop(EXPANDABLE_THRESHOLD) - .toImmutableList(), - ) - } -} + items(relatedLocations.take(EXPANDABLE_THRESHOLD)) { + ItemLocationListItem( + vectorPainter = rememberVectorPainter(it.icon), + iconTestTag = "ItemLocationIcon", + text = it.name, + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } -@Composable -private fun ColumnScope.ExpandingItemLocationContent( - overflowLocations: ImmutableList, -) { - var isExpanded by rememberSaveable { mutableStateOf(false) } - AnimatedVisibility( - visible = isExpanded, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically(), - modifier = Modifier.clipToBounds(), - ) { - LazyColumn { - items(overflowLocations) { + if (isExpanded) { + items(relatedLocations.drop(EXPANDABLE_THRESHOLD)) { ItemLocationListItem( vectorPainter = rememberVectorPainter(it.icon), text = it.name, iconTestTag = "ItemLocationIcon", modifier = Modifier .padding(horizontal = 16.dp) + .animateItem() .fillMaxWidth(), ) } } - } - if (overflowLocations.isNotEmpty()) { - BitwardenExpandingHeader( - collapsedText = stringResource(R.string.show_more), - expandedText = stringResource(R.string.show_less), - isExpanded = isExpanded, - onClick = { isExpanded = !isExpanded }, - showExpansionIndicator = false, - ) + if (relatedLocations.size > EXPANDABLE_THRESHOLD) { + item { + BitwardenExpandingHeader( + collapsedText = stringResource(R.string.show_more), + expandedText = stringResource(R.string.show_less), + isExpanded = isExpanded, + onClick = { isExpanded = !isExpanded }, + showExpansionIndicator = false, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } } } @@ -194,31 +173,35 @@ private fun ItemHeaderIcon( modifier: Modifier = Modifier, testTag: String? = null, ) { + val isLocalIcon = iconData is IconData.Local Box( contentAlignment = Alignment.Center, - modifier = if (iconData is IconData.Local) { - modifier.then( + modifier = modifier.then( + if (isLocalIcon) { Modifier.background( color = BitwardenTheme.colorScheme.illustration.backgroundPrimary, shape = BitwardenTheme.shapes.favicon, - ), - ) - } else { - modifier - }, + ) + } else { + Modifier + }, + ), ) { BitwardenIcon( iconData = iconData, contentDescription = null, tint = BitwardenTheme.colorScheme.illustration.outline, modifier = Modifier - .nullableTestTag(testTag), + .nullableTestTag(testTag) + .then( + if (!isLocalIcon) Modifier.fillMaxSize() else Modifier, + ), ) } } @Composable -private fun ItemLocationListItem( +private fun LazyItemScope.ItemLocationListItem( vectorPainter: VectorPainter, iconTestTag: String?, text: String, @@ -254,18 +237,14 @@ private fun ItemLocationListItem( @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ItemHeader_LocalIcon_Preview() { BitwardenTheme { - LazyColumn { - item { - ItemHeader( - value = "Login without favicon", - isFavorite = true, - iconData = IconData.Local( - iconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf(), - ) - } - } + ItemHeader( + value = "Login without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_globe, + ), + relatedLocations = persistentListOf(), + ) } } @@ -273,19 +252,15 @@ private fun ItemHeader_LocalIcon_Preview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ItemHeader_NetworkIcon_Preview() { BitwardenTheme { - LazyColumn { - item { - ItemHeader( - value = "Login with favicon", - isFavorite = true, - iconData = IconData.Network( - uri = "mockuri", - fallbackIconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf(), - ) - } - } + ItemHeader( + value = "Login with favicon", + isFavorite = true, + iconData = IconData.Network( + uri = "mockuri", + fallbackIconRes = R.drawable.ic_globe, + ), + relatedLocations = persistentListOf(), + ) } } @@ -293,20 +268,16 @@ private fun ItemHeader_NetworkIcon_Preview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) private fun ItemHeader_Organization_Preview() { BitwardenTheme { - LazyColumn { - item { - ItemHeader( - value = "Login without favicon", - isFavorite = true, - iconData = IconData.Local( - iconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf( - VaultItemLocation.Organization("Stark Industries"), - ), - ) - } - } + ItemHeader( + value = "Login without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_globe, + ), + relatedLocations = persistentListOf( + VaultItemLocation.Organization("Stark Industries"), + ), + ) } } @@ -314,21 +285,17 @@ private fun ItemHeader_Organization_Preview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ItemNameField_Org_SingleCollection_Preview() { BitwardenTheme { - LazyColumn { - item { - ItemHeader( - value = "Login without favicon", - isFavorite = true, - iconData = IconData.Local( - iconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf( - VaultItemLocation.Organization("Stark Industries"), - VaultItemLocation.Collection("Marketing"), - ), - ) - } - } + ItemHeader( + value = "Login without favicon", + isFavorite = true, + iconData = IconData.Local( + iconRes = R.drawable.ic_globe, + ), + relatedLocations = persistentListOf( + VaultItemLocation.Organization("Stark Industries"), + VaultItemLocation.Collection("Marketing"), + ), + ) } } @@ -336,22 +303,18 @@ private fun ItemNameField_Org_SingleCollection_Preview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ItemNameField_Org_MultiCollection_Preview() { BitwardenTheme { - LazyColumn { - item { - 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"), - ), - ) - } - } + 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"), + ), + ) } } @@ -359,22 +322,18 @@ private fun ItemNameField_Org_MultiCollection_Preview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ItemNameField_Org_SingleCollection_Folder_Preview() { BitwardenTheme { - LazyColumn { - item { - 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"), - ), - ) - } - } + 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"), + ), + ) } } //endregion Previews From a31c499b156c337bcd67e34cdf65b27ec538c7bb Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 25 Feb 2025 17:48:43 -0500 Subject: [PATCH 16/27] Remove unnecessary mockkStatic call in CipherViewExtensionsTest.kt --- .../ui/vault/feature/item/util/CipherViewExtensionsTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt index b9fe440e48..a938e4293d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt @@ -28,7 +28,6 @@ class CipherViewExtensionsTest { @BeforeEach fun setUp() { - mockkStatic(Uri::class) setupMockUri() } From 7cc8108498ad9206aae5bca2c015e1937322867c Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 25 Feb 2025 18:10:28 -0500 Subject: [PATCH 17/27] Adjust `ItemHeader` expanding header fill the available width --- .../bitwarden/ui/vault/feature/item/component/ItemHeader.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 0d6368fcde..9e0520606b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -160,7 +160,9 @@ fun ItemHeader( isExpanded = isExpanded, onClick = { isExpanded = !isExpanded }, showExpansionIndicator = false, - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) } } From af8cfcd2f041bf9fcd9dbccbd2918ebb3eb7b628 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 25 Feb 2025 18:11:50 -0500 Subject: [PATCH 18/27] Move expansion indicator outside of crossfade animation --- .../header/BitwardenExpandingHeader.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt index c2f78bc84e..8107be98d5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt @@ -81,18 +81,18 @@ fun BitwardenExpandingHeader( 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), - ) - } + } + 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), + ) } } } From 88609c2f5b9fd9915c9aa3a444052a3e6b9e795a Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Wed, 26 Feb 2025 08:58:54 -0500 Subject: [PATCH 19/27] Update OrganizationType.OWNER mock data in VaultItemViewModelTest.kt --- .../bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 816324e229..24aaefef43 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -3341,6 +3341,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { shouldManageResetPassword = false, shouldUseKeyConnector = false, role = OrganizationType.OWNER, + shouldManagePolicies = false, ), ), ), From 33c3fd28e9d202ee36515450068804c264d064db Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 26 Feb 2025 14:45:03 -0600 Subject: [PATCH 20/27] itemHeader as LazyListScope extension --- .../header/BitwardenExpandingHeader.kt | 8 +- .../feature/item/VaultItemCardContent.kt | 468 +++++++-------- .../feature/item/VaultItemIdentityContent.kt | 551 +++++++++--------- .../feature/item/VaultItemLoginContent.kt | 438 +++++++------- .../item/VaultItemSecureNoteContent.kt | 218 +++---- .../feature/item/VaultItemSshKeyContent.kt | 342 +++++------ .../feature/item/component/ItemHeader.kt | 447 +++++++------- 7 files changed, 1269 insertions(+), 1203 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt index 8107be98d5..b0e9dafb3f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenExpandingHeader.kt @@ -5,6 +5,7 @@ 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 @@ -15,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 @@ -37,10 +39,12 @@ fun BitwardenExpandingHeader( 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, @@ -48,8 +52,8 @@ 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, ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index b0bcdd3b1b..ef8ecc220a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.vault.feature.item -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -10,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 @@ -23,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.ItemHeader 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 @@ -42,254 +45,257 @@ fun VaultItemCardContent( vaultCardItemTypeHandlers: VaultCardItemTypeHandlers, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { - Spacer(Modifier.height(height = 12.dp)) - ItemHeader( + 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 }, ) - LazyColumn(modifier = modifier) { - cardState.cardholderName?.let { cardholderName -> - item { - BitwardenTextField( - label = stringResource(id = R.string.cardholder_name), - value = cardholderName, - onValueChange = {}, - readOnly = true, - singleLine = false, - textFieldTestTag = "CardholderNameEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = cardholderName), - dividerPadding = 0.dp, + cardState.cardholderName?.let { cardholderName -> + item { + BitwardenTextField( + label = stringResource(id = R.string.cardholder_name), + value = cardholderName, + onValueChange = {}, + readOnly = true, + singleLine = false, + textFieldTestTag = "CardholderNameEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = cardholderName), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + cardState.number?.let { numberData -> + item { + BitwardenPasswordField( + label = stringResource(id = R.string.number), + value = numberData.number, + onValueChange = {}, + showPassword = numberData.isVisible, + showPasswordChange = vaultCardItemTypeHandlers.onShowNumberClick, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_number), + onClick = vaultCardItemTypeHandlers.onCopyNumberClick, + modifier = Modifier.testTag(tag = "CardCopyNumberButton"), + ) + }, + passwordFieldTestTag = "CardNumberEntry", + showPasswordTestTag = "CardViewNumberButton", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = numberData), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) { + item { + BitwardenTextField( + label = stringResource(id = R.string.brand), + value = cardState.brand.shortName(), + onValueChange = {}, + readOnly = true, + singleLine = false, + textFieldTestTag = "CardBrandEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = cardState.brand), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + cardState.expiration?.let { expiration -> + item { + BitwardenTextField( + label = stringResource(id = R.string.expiration), + value = expiration, + onValueChange = {}, + readOnly = true, + singleLine = false, + textFieldTestTag = "CardExpirationEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = expiration), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + cardState.securityCode?.let { securityCodeData -> + item { + BitwardenPasswordField( + label = stringResource(id = R.string.security_code), + value = securityCodeData.code, + onValueChange = {}, + showPassword = securityCodeData.isVisible, + showPasswordChange = vaultCardItemTypeHandlers.onShowSecurityCodeClick, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource( + id = R.string.copy_security_code, ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - cardState.number?.let { numberData -> - item { - BitwardenPasswordField( - label = stringResource(id = R.string.number), - value = numberData.number, - onValueChange = {}, - showPassword = numberData.isVisible, - showPasswordChange = vaultCardItemTypeHandlers.onShowNumberClick, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_number), - onClick = vaultCardItemTypeHandlers.onCopyNumberClick, - modifier = Modifier.testTag(tag = "CardCopyNumberButton"), - ) - }, - passwordFieldTestTag = "CardNumberEntry", - showPasswordTestTag = "CardViewNumberButton", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = numberData), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) { - item { - BitwardenTextField( - label = stringResource(id = R.string.brand), - value = cardState.brand.shortName(), - onValueChange = {}, - readOnly = true, - singleLine = false, - textFieldTestTag = "CardBrandEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = cardState.brand), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - cardState.expiration?.let { expiration -> - item { - BitwardenTextField( - label = stringResource(id = R.string.expiration), - value = expiration, - onValueChange = {}, - readOnly = true, - singleLine = false, - textFieldTestTag = "CardExpirationEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = expiration), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - cardState.securityCode?.let { securityCodeData -> - item { - BitwardenPasswordField( - label = stringResource(id = R.string.security_code), - value = securityCodeData.code, - onValueChange = {}, - showPassword = securityCodeData.isVisible, - showPasswordChange = vaultCardItemTypeHandlers.onShowSecurityCodeClick, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource( - id = R.string.copy_security_code, - ), - onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick, - modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"), - ) - }, - showPasswordTestTag = "CardViewSecurityCodeButton", - passwordFieldTestTag = "CardSecurityCodeEntry", - cardStyle = cardState - .propertyList - .toListItemCardStyle( - index = cardState.propertyList.indexOf(element = securityCodeData), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.notes?.let { notes -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } + onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick, + modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"), + ) + }, + showPasswordTestTag = "CardViewSecurityCodeButton", + passwordFieldTestTag = "CardSecurityCodeEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = securityCodeData), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } + } + commonState.notes?.let { notes -> item { Spacer(modifier = Modifier.height(height = 16.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_updated)}: ", - text = commonState.lastUpdated, + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 12.dp), + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .testTag("CipherAttachment") + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } + } + + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp), + ) + } + item { + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index cabc7f601f..1501b079e4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.vault.feature.item -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -10,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 +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.ItemHeader 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 @@ -39,302 +42,304 @@ fun VaultItemIdentityContent( vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { - Spacer(Modifier.height(height = 12.dp)) - ItemHeader( + 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", - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), + isExpanded = isExpanded, + onExpandClick = { isExpanded = !isExpanded }, ) - LazyColumn { - identityState.identityName?.let { identityName -> - item { - IdentityCopyField( - label = stringResource(id = R.string.identity_name), - value = identityName, - copyContentDescription = stringResource(id = R.string.copy_identity_name), - textFieldTestTag = "IdentityNameEntry", - copyActionTestTag = "IdentityCopyNameButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = identityName), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + identityState.identityName?.let { identityName -> + item { + IdentityCopyField( + label = stringResource(id = R.string.identity_name), + value = identityName, + copyContentDescription = stringResource(id = R.string.copy_identity_name), + textFieldTestTag = "IdentityNameEntry", + copyActionTestTag = "IdentityCopyNameButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = identityName), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.username?.let { username -> - item { - IdentityCopyField( - label = stringResource(id = R.string.username), - value = username, - copyContentDescription = stringResource(id = R.string.copy_username), - textFieldTestTag = "IdentityUsernameEntry", - copyActionTestTag = "IdentityCopyUsernameButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = username), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.username?.let { username -> + item { + IdentityCopyField( + label = stringResource(id = R.string.username), + value = username, + copyContentDescription = stringResource(id = R.string.copy_username), + textFieldTestTag = "IdentityUsernameEntry", + copyActionTestTag = "IdentityCopyUsernameButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = username), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.company?.let { company -> - item { - IdentityCopyField( - label = stringResource(id = R.string.company), - value = company, - copyContentDescription = stringResource(id = R.string.copy_company), - textFieldTestTag = "IdentityCompanyEntry", - copyActionTestTag = "IdentityCopyCompanyButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = company), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.company?.let { company -> + item { + IdentityCopyField( + label = stringResource(id = R.string.company), + value = company, + copyContentDescription = stringResource(id = R.string.copy_company), + textFieldTestTag = "IdentityCompanyEntry", + copyActionTestTag = "IdentityCopyCompanyButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = company), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.ssn?.let { ssn -> - item { - IdentityCopyField( - label = stringResource(id = R.string.ssn), - value = ssn, - copyContentDescription = stringResource(id = R.string.copy_ssn), - textFieldTestTag = "IdentitySsnEntry", - copyActionTestTag = "IdentityCopySsnButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = ssn), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.ssn?.let { ssn -> + item { + IdentityCopyField( + label = stringResource(id = R.string.ssn), + value = ssn, + copyContentDescription = stringResource(id = R.string.copy_ssn), + textFieldTestTag = "IdentitySsnEntry", + copyActionTestTag = "IdentityCopySsnButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = ssn), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.passportNumber?.let { passportNumber -> - item { - IdentityCopyField( - label = stringResource(id = R.string.passport_number), - value = passportNumber, - copyContentDescription = stringResource(id = R.string.copy_passport_number), - textFieldTestTag = "IdentityPassportNumberEntry", - copyActionTestTag = "IdentityCopyPassportNumberButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = passportNumber), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.passportNumber?.let { passportNumber -> + item { + IdentityCopyField( + label = stringResource(id = R.string.passport_number), + value = passportNumber, + copyContentDescription = stringResource(id = R.string.copy_passport_number), + textFieldTestTag = "IdentityPassportNumberEntry", + copyActionTestTag = "IdentityCopyPassportNumberButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = passportNumber), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.licenseNumber?.let { licenseNumber -> - item { - IdentityCopyField( - label = stringResource(id = R.string.license_number), - value = licenseNumber, - copyContentDescription = stringResource(id = R.string.copy_license_number), - textFieldTestTag = "IdentityLicenseNumberEntry", - copyActionTestTag = "IdentityCopyLicenseNumberButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = licenseNumber), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.licenseNumber?.let { licenseNumber -> + item { + IdentityCopyField( + label = stringResource(id = R.string.license_number), + value = licenseNumber, + copyContentDescription = stringResource(id = R.string.copy_license_number), + textFieldTestTag = "IdentityLicenseNumberEntry", + copyActionTestTag = "IdentityCopyLicenseNumberButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = licenseNumber), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.email?.let { email -> - item { - IdentityCopyField( - label = stringResource(id = R.string.email), - value = email, - copyContentDescription = stringResource(id = R.string.copy_email), - textFieldTestTag = "IdentityEmailEntry", - copyActionTestTag = "IdentityCopyEmailButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = email), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.email?.let { email -> + item { + IdentityCopyField( + label = stringResource(id = R.string.email), + value = email, + copyContentDescription = stringResource(id = R.string.copy_email), + textFieldTestTag = "IdentityEmailEntry", + copyActionTestTag = "IdentityCopyEmailButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = email), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.phone?.let { phone -> - item { - IdentityCopyField( - label = stringResource(id = R.string.phone), - value = phone, - copyContentDescription = stringResource(id = R.string.copy_phone), - textFieldTestTag = "IdentityPhoneEntry", - copyActionTestTag = "IdentityCopyPhoneButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = phone), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.phone?.let { phone -> + item { + IdentityCopyField( + label = stringResource(id = R.string.phone), + value = phone, + copyContentDescription = stringResource(id = R.string.copy_phone), + textFieldTestTag = "IdentityPhoneEntry", + copyActionTestTag = "IdentityCopyPhoneButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = phone), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - identityState.address?.let { address -> - item { - IdentityCopyField( - label = stringResource(id = R.string.address), - value = address, - copyContentDescription = stringResource(id = R.string.copy_address), - textFieldTestTag = "IdentityAddressEntry", - copyActionTestTag = "IdentityCopyAddressButton", - onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick, - cardStyle = identityState - .propertyList - .toListItemCardStyle( - index = identityState.propertyList.indexOf(element = address), - dividerPadding = 0.dp, - ), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } + } + identityState.address?.let { address -> + item { + IdentityCopyField( + label = stringResource(id = R.string.address), + value = address, + copyContentDescription = stringResource(id = R.string.copy_address), + textFieldTestTag = "IdentityAddressEntry", + copyActionTestTag = "IdentityCopyAddressButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = address), + dividerPadding = 0.dp, + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } - commonState.notes?.let { notes -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - IdentityCopyField( - label = stringResource(id = R.string.notes), - value = notes, - copyContentDescription = stringResource(id = R.string.copy_notes), - textFieldTestTag = "CipherNotesLabel", - copyActionTestTag = "CipherNotesCopyButton", - onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } - + } + commonState.notes?.let { notes -> item { Spacer(modifier = Modifier.height(height = 16.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_updated)}: ", - text = commonState.lastUpdated, + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 12.dp), + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + IdentityCopyField( + label = stringResource(id = R.string.notes), + value = notes, + copyContentDescription = stringResource(id = R.string.copy_notes), + textFieldTestTag = "CipherNotesLabel", + copyActionTestTag = "CipherNotesCopyButton", + onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .testTag("CipherAttachment") + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } + } + + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp), + ) + } + item { + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 89ea43d819..fea1811028 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.vault.feature.item -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -12,6 +11,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 @@ -31,8 +34,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.ItemHeader 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 @@ -51,229 +54,248 @@ fun VaultItemLoginContent( vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .standardHorizontalMargin(), + var isExpanded by rememberSaveable { mutableStateOf(value = false) } + LazyColumn( + modifier = modifier.fillMaxWidth(), ) { - Spacer(Modifier.height(height = 12.dp)) - ItemHeader( + item { + Spacer(Modifier.height(height = 12.dp)) + } + itemHeader( value = commonState.name, isFavorite = commonState.favorite, iconData = commonState.iconData, relatedLocations = commonState.relatedLocations, iconTestTag = "LoginItemNameIcon", textFieldTestTag = "LoginItemNameEntry", + isExpanded = isExpanded, + onExpandClick = { isExpanded = !isExpanded }, ) - LazyColumn( - modifier = modifier, - ) { - if (loginItemState.hasLoginCredentials) { - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.login_credentials), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - } - - loginItemState.username?.let { username -> - item { - UsernameField( - username = username, - onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick, - cardStyle = loginItemState - .passwordData - ?.let { CardStyle.Top(dividerPadding = 0.dp) } - ?: CardStyle.Full, - modifier = Modifier - .fillMaxWidth(), - ) - } - } - - loginItemState.passwordData?.let { passwordData -> - item { - PasswordField( - passwordData = passwordData, - onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick, - onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick, - onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick, - cardStyle = loginItemState - .username - ?.let { CardStyle.Bottom } - ?: CardStyle.Full, - modifier = Modifier - .fillMaxWidth(), - ) - } - } - - loginItemState.fido2CredentialCreationDateText?.let { creationDate -> - item { - Spacer(modifier = Modifier.height(8.dp)) - Fido2CredentialField( - creationDate = creationDate(), - modifier = Modifier - .fillMaxWidth(), - ) - } - } - - loginItemState.totpCodeItemData?.let { totpCodeItemData -> - item { - Spacer(modifier = Modifier.height(8.dp)) - TotpField( - totpCodeItemData = totpCodeItemData, - enabled = loginItemState.canViewTotpCode, - onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick, - onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers - .onAuthenticatorHelpToolTipClick, - modifier = Modifier - .fillMaxWidth(), - ) - } - } - - loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.autofill_options), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - - itemsIndexed(uris) { index, uriData -> - UriField( - uriData = uriData, - onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick, - onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick, - cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp), - modifier = Modifier - .fillMaxWidth(), - ) - } - } - - commonState.notes?.let { notes -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), - modifier = Modifier - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(8.dp)) - NotesField( - notes = notes, - onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier - .fillMaxWidth(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth(), - ) - } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .fillMaxWidth(), - attachmentItem = attachmentItem, - cardStyle = attachments.toListItemCardStyle(index = index), - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - ) - } - } - + if (loginItemState.hasLoginCredentials) { item { - Spacer(modifier = Modifier.height(16.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_updated)}: ", - text = commonState.lastUpdated, + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.login_credentials), modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + } + + loginItemState.username?.let { username -> + item { + UsernameField( + username = username, + onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick, + cardStyle = loginItemState + .passwordData + ?.let { CardStyle.Top(dividerPadding = 0.dp) } + ?: CardStyle.Full, + modifier = Modifier + .standardHorizontalMargin() .fillMaxWidth(), ) } + } - loginItemState.passwordRevisionDate?.let { revisionDate -> - item { - Spacer(modifier = Modifier.height(height = 4.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_password_updated)}: ", - text = revisionDate, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp), - ) - } - } - - loginItemState.passwordHistoryCount?.let { passwordHistoryCount -> - item { - Spacer(modifier = Modifier.height(height = 4.dp)) - BitwardenHyperTextLink( - annotatedResId = R.string.password_history_count, - args = arrayOf(passwordHistoryCount.toString()), - annotationKey = "passwordHistory", - accessibilityString = stringResource(id = R.string.password_history), - onClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier - .wrapContentWidth() - .padding(horizontal = 12.dp), - ) - } - } - + loginItemState.passwordData?.let { passwordData -> item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) + PasswordField( + passwordData = passwordData, + onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick, + onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick, + onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick, + cardStyle = loginItemState + .username + ?.let { CardStyle.Bottom } + ?: CardStyle.Full, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) } } + + loginItemState.fido2CredentialCreationDateText?.let { creationDate -> + item { + Spacer(modifier = Modifier.height(8.dp)) + Fido2CredentialField( + creationDate = creationDate(), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + } + } + + loginItemState.totpCodeItemData?.let { totpCodeItemData -> + item { + Spacer(modifier = Modifier.height(8.dp)) + TotpField( + totpCodeItemData = totpCodeItemData, + enabled = loginItemState.canViewTotpCode, + onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick, + onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers + .onAuthenticatorHelpToolTipClick, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + } + } + + loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.autofill_options), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + + itemsIndexed(uris) { index, uriData -> + UriField( + uriData = uriData, + onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick, + onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick, + cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + } + } + + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), + modifier = Modifier + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + NotesField( + notes = notes, + onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + attachmentItem = attachmentItem, + cardStyle = attachments.toListItemCardStyle(index = index), + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + ) + } + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, + modifier = Modifier + .standardHorizontalMargin() + .padding(horizontal = 12.dp) + .fillMaxWidth(), + ) + } + + loginItemState.passwordRevisionDate?.let { revisionDate -> + item { + Spacer(modifier = Modifier.height(height = 4.dp)) + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_password_updated)}: ", + text = revisionDate, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp), + ) + } + } + + loginItemState.passwordHistoryCount?.let { passwordHistoryCount -> + item { + Spacer(modifier = Modifier.height(height = 4.dp)) + BitwardenHyperTextLink( + annotatedResId = R.string.password_history_count, + args = arrayOf(passwordHistoryCount.toString()), + annotationKey = "passwordHistory", + accessibilityString = stringResource(id = R.string.password_history), + onClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick, + style = BitwardenTheme.typography.labelMedium, + modifier = Modifier + .wrapContentWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp), + ) + } + } + + item { + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index 3655c59aa7..ea32ae974f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.vault.feature.item -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -12,6 +11,10 @@ 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 @@ -26,7 +29,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.ItemHeader +import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers /** @@ -39,129 +42,128 @@ fun VaultItemSecureNoteContent( vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { - Spacer(Modifier.height(height = 12.dp)) - ItemHeader( + 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 = "SecureNoteItemNameIcon", textFieldTestTag = "SecureNoteItemNameEntry", - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), + isExpanded = isExpanded, + onExpandClick = { isExpanded = !isExpanded }, ) - LazyColumn { - - commonState.notes?.let { notes -> - item { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - } - - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } + } + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { Spacer(modifier = Modifier.height(height = 16.dp)) - Row( + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .semantics(mergeDescendants = true) { }, - ) { - Text( - text = "${stringResource(id = R.string.date_updated)}: ", - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - Text( - text = commonState.lastUpdated, - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - } + .padding(horizontal = 16.dp), + ) } - item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .testTag("CipherAttachment") + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } + } + + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp) + .semantics(mergeDescendants = true) { }, + ) { + Text( + text = "${stringResource(id = R.string.date_updated)}: ", + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.primary, + ) + Text( + text = commonState.lastUpdated, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.primary, + ) + } + } + + item { + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt index 5cd6a633ec..729a043a2f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.vault.feature.item -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -10,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 @@ -23,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.ItemHeader 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 @@ -40,195 +43,198 @@ fun VaultItemSshKeyContent( vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { - Spacer(Modifier.height(height = 12.dp)) - ItemHeader( + 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 = "SshKeyItemNameIcon", textFieldTestTag = "SshKeyItemNameEntry", + isExpanded = isExpanded, + onExpandClick = { isExpanded = !isExpanded }, ) - LazyColumn(modifier = modifier) { - item { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.public_key), - value = sshKeyItemState.publicKey, - onValueChange = { }, - singleLine = false, - readOnly = true, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_public_key), - onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick, - modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"), - ) - }, - cardStyle = CardStyle.Top(), - modifier = Modifier - .testTag("SshKeyItemPublicKeyEntry") - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - - item { - BitwardenPasswordField( - label = stringResource(id = R.string.private_key), - value = sshKeyItemState.privateKey, - onValueChange = { }, - singleLine = false, - readOnly = true, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_private_key), - onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick, - modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"), - ) - }, - showPassword = sshKeyItemState.showPrivateKey, - showPasswordTestTag = "ViewPrivateKeyButton", - showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick, - cardStyle = CardStyle.Middle(), - modifier = Modifier - .testTag("SshKeyItemPrivateKeyEntry") - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - - item { - BitwardenTextField( - label = stringResource(id = R.string.fingerprint), - value = sshKeyItemState.fingerprint, - onValueChange = { }, - singleLine = false, - readOnly = true, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_fingerprint), - onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick, - modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"), - ) - }, - cardStyle = CardStyle.Bottom, - modifier = Modifier - .testTag("SshKeyItemFingerprintEntry") - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - - commonState.notes?.let { notes -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), + item { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.public_key), + value = sshKeyItemState.publicKey, + onValueChange = { }, + singleLine = false, + readOnly = true, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_public_key), + onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick, + modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"), ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } + }, + cardStyle = CardStyle.Top(), + modifier = Modifier + .testTag("SshKeyItemPublicKeyEntry") + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), + item { + BitwardenPasswordField( + label = stringResource(id = R.string.private_key), + value = sshKeyItemState.privateKey, + onValueChange = { }, + singleLine = false, + readOnly = true, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_private_key), + onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick, + modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"), ) - } - items(customFields) { customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - } - } + }, + showPassword = sshKeyItemState.showPrivateKey, + showPasswordTestTag = "ViewPrivateKeyButton", + showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick, + cardStyle = CardStyle.Middle(), + modifier = Modifier + .testTag("SshKeyItemPrivateKeyEntry") + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), + item { + BitwardenTextField( + label = stringResource(id = R.string.fingerprint), + value = sshKeyItemState.fingerprint, + onValueChange = { }, + singleLine = false, + readOnly = true, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_fingerprint), + onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick, + modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"), ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed(attachments) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } + }, + cardStyle = CardStyle.Bottom, + modifier = Modifier + .testTag("SshKeyItemFingerprintEntry") + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + commonState.notes?.let { notes -> item { Spacer(modifier = Modifier.height(height = 16.dp)) - VaultItemUpdateText( - header = "${stringResource(id = R.string.date_updated)}: ", - text = commonState.lastUpdated, + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .testTag("SshKeyItemLastUpdated"), + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } + } + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(88.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) } + items(customFields) { customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = + vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + } + + commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed(attachments) { index, attachmentItem -> + AttachmentItemContent( + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + ) + } + } + + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + VaultItemUpdateText( + header = "${stringResource(id = R.string.date_updated)}: ", + text = commonState.lastUpdated, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp) + .testTag("SshKeyItemLastUpdated"), + ) + } + + item { + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 9e0520606b..ed3d999357 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -1,38 +1,33 @@ package com.x8bit.bitwarden.ui.vault.feature.item.component -import android.content.res.Configuration 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.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height 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.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.saveable.rememberSaveable -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.components.divider.BitwardenHorizontalDivider +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 @@ -42,7 +37,6 @@ 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. @@ -53,118 +47,145 @@ private const val EXPANDABLE_THRESHOLD = 2 * Reusable composable for displaying the cipher name, favorite status, and related locations. */ @Suppress("LongMethod") -@Composable -fun ItemHeader( +fun LazyListScope.itemHeader( value: String, isFavorite: Boolean, relatedLocations: ImmutableList, iconData: IconData, - modifier: Modifier = Modifier, + isExpanded: Boolean, iconTestTag: String? = null, textFieldTestTag: String? = null, + onExpandClick: () -> Unit, ) { - var isExpanded by rememberSaveable { mutableStateOf(false) } - LazyColumn( - modifier = modifier - .cardStyle(CardStyle.Full) - .fillMaxWidth(), - ) { - item { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) { - ItemHeaderIcon( - iconData = iconData, - testTag = iconTestTag, - modifier = Modifier.size(36.dp), + item { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .defaultMinSize(minHeight = 60.dp) + .cardStyle( + cardStyle = CardStyle.Top(), + paddingVertical = 0.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, - ) - } - BitwardenHorizontalDivider(Modifier.padding(start = 16.dp)) - - Spacer(Modifier.height(8.dp)) - } - - if (relatedLocations.isEmpty()) { - item { - ItemLocationListItem( - vectorPainter = rememberVectorPainter(R.drawable.ic_folder), - text = stringResource(R.string.no_folder), - iconTestTag = "NoFolderIcon", - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } - return@LazyColumn - } - - items(relatedLocations.take(EXPANDABLE_THRESHOLD)) { - ItemLocationListItem( - vectorPainter = rememberVectorPainter(it.icon), - iconTestTag = "ItemLocationIcon", - text = it.name, - modifier = Modifier - .animateItem() - .fillMaxWidth() - .padding(horizontal = 16.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 (isExpanded) { - items(relatedLocations.drop(EXPANDABLE_THRESHOLD)) { - ItemLocationListItem( - vectorPainter = rememberVectorPainter(it.icon), - text = it.name, - iconTestTag = "ItemLocationIcon", - modifier = Modifier - .padding(horizontal = 16.dp) - .animateItem() - .fillMaxWidth(), - ) - } + if (relatedLocations.isEmpty()) { + item { + ItemLocationListItem( + vectorPainter = rememberVectorPainter(R.drawable.ic_folder), + text = stringResource(R.string.no_folder), + iconTestTag = "NoFolderIcon", + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth() + .cardStyle( + cardStyle = CardStyle.Bottom, + paddingVertical = 0.dp, + ), + ) } + return + } - if (relatedLocations.size > EXPANDABLE_THRESHOLD) { - item { - BitwardenExpandingHeader( - collapsedText = stringResource(R.string.show_more), - expandedText = stringResource(R.string.show_less), - isExpanded = isExpanded, - onClick = { isExpanded = !isExpanded }, - showExpansionIndicator = false, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } + items( + key = { "locations_$it" }, + items = relatedLocations.take(EXPANDABLE_THRESHOLD), + ) { + ItemLocationListItem( + vectorPainter = rememberVectorPainter(it.icon), + iconTestTag = "ItemLocationIcon", + text = it.name, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .animateItem() + .cardStyle( + cardStyle = if (relatedLocations.size > EXPANDABLE_THRESHOLD) { + CardStyle.Middle(hasDivider = false) + } else { + CardStyle.Bottom + }, + 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, + ), + ) } } } @@ -235,107 +256,107 @@ private fun LazyItemScope.ItemLocationListItem( } //region Previews -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -private fun ItemHeader_LocalIcon_Preview() { - BitwardenTheme { - ItemHeader( - value = "Login without favicon", - isFavorite = true, - iconData = IconData.Local( - iconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf(), - ) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -private fun ItemHeader_NetworkIcon_Preview() { - BitwardenTheme { - ItemHeader( - value = "Login with favicon", - isFavorite = true, - iconData = IconData.Network( - uri = "mockuri", - fallbackIconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf(), - ) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun ItemHeader_Organization_Preview() { - BitwardenTheme { - ItemHeader( - value = "Login without favicon", - isFavorite = true, - iconData = IconData.Local( - iconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf( - VaultItemLocation.Organization("Stark Industries"), - ), - ) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -private fun ItemNameField_Org_SingleCollection_Preview() { - BitwardenTheme { - ItemHeader( - value = "Login without favicon", - isFavorite = true, - iconData = IconData.Local( - iconRes = R.drawable.ic_globe, - ), - relatedLocations = persistentListOf( - VaultItemLocation.Organization("Stark Industries"), - VaultItemLocation.Collection("Marketing"), - ), - ) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -private fun ItemNameField_Org_MultiCollection_Preview() { - BitwardenTheme { - 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"), - ), - ) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -private fun ItemNameField_Org_SingleCollection_Folder_Preview() { - BitwardenTheme { - 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"), - ), - ) - } -} +//@Composable +//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +//private fun ItemHeader_LocalIcon_Preview() { +// BitwardenTheme { +// ItemHeader( +// value = "Login without favicon", +// isFavorite = true, +// iconData = IconData.Local( +// iconRes = R.drawable.ic_globe, +// ), +// relatedLocations = persistentListOf(), +// ) +// } +//} +// +//@Composable +//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +//private fun ItemHeader_NetworkIcon_Preview() { +// BitwardenTheme { +// ItemHeader( +// value = "Login with favicon", +// isFavorite = true, +// iconData = IconData.Network( +// uri = "mockuri", +// fallbackIconRes = R.drawable.ic_globe, +// ), +// relatedLocations = persistentListOf(), +// ) +// } +//} +// +//@Composable +//@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +//private fun ItemHeader_Organization_Preview() { +// BitwardenTheme { +// ItemHeader( +// value = "Login without favicon", +// isFavorite = true, +// iconData = IconData.Local( +// iconRes = R.drawable.ic_globe, +// ), +// relatedLocations = persistentListOf( +// VaultItemLocation.Organization("Stark Industries"), +// ), +// ) +// } +//} +// +//@Composable +//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +//private fun ItemNameField_Org_SingleCollection_Preview() { +// BitwardenTheme { +// ItemHeader( +// value = "Login without favicon", +// isFavorite = true, +// iconData = IconData.Local( +// iconRes = R.drawable.ic_globe, +// ), +// relatedLocations = persistentListOf( +// VaultItemLocation.Organization("Stark Industries"), +// VaultItemLocation.Collection("Marketing"), +// ), +// ) +// } +//} +// +//@Composable +//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +//private fun ItemNameField_Org_MultiCollection_Preview() { +// BitwardenTheme { +// 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"), +// ), +// ) +// } +//} +// +//@Composable +//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +//private fun ItemNameField_Org_SingleCollection_Folder_Preview() { +// BitwardenTheme { +// 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"), +// ), +// ) +// } +//} //endregion Previews From 07d3849c4bbe71ab4080d288b13b7636450e7329 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Wed, 26 Feb 2025 17:10:10 -0500 Subject: [PATCH 21/27] Add keys and animation to all content items --- .../feature/item/VaultItemCardContent.kt | 70 ++++++++------ .../feature/item/VaultItemIdentityContent.kt | 84 ++++++++++------- .../feature/item/VaultItemLoginContent.kt | 93 ++++++++++++------- .../item/VaultItemSecureNoteContent.kt | 43 +++++---- .../feature/item/VaultItemSshKeyContent.kt | 59 +++++++----- .../feature/item/component/ItemHeader.kt | 50 +++++----- .../feature/item/VaultItemViewModelTest.kt | 1 - 7 files changed, 245 insertions(+), 155 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index ef8ecc220a..0c0f3db4a1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -61,7 +61,7 @@ fun VaultItemCardContent( onExpandClick = { isExpanded = !isExpanded }, ) cardState.cardholderName?.let { cardholderName -> - item { + item(key = "cardholderName") { BitwardenTextField( label = stringResource(id = R.string.cardholder_name), value = cardholderName, @@ -77,12 +77,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, @@ -109,13 +110,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(), @@ -131,13 +133,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, @@ -153,13 +156,14 @@ fun VaultItemCardContent( ), modifier = Modifier .fillMaxWidth() - .standardHorizontalMargin(), + .standardHorizontalMargin() + .animateItem(), ) } } cardState.securityCode?.let { securityCodeData -> - item { + item(key = "securityCodeData_$securityCodeData") { BitwardenPasswordField( label = stringResource(id = R.string.security_code), value = securityCodeData.code, @@ -188,20 +192,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( @@ -222,58 +228,69 @@ 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, onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, + vaultCommonItemTypeHandlers.onCopyCustomTextField, onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, 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, @@ -282,7 +299,7 @@ fun VaultItemCardContent( } } - item { + item(key = "lastUpdated") { Spacer(modifier = Modifier.height(height = 16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", @@ -290,7 +307,8 @@ fun VaultItemCardContent( modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 12.dp), + .padding(horizontal = 12.dp) + .animateItem(), ) } item { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index 1501b079e4..72069334f8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -60,7 +60,7 @@ fun VaultItemIdentityContent( onExpandClick = { isExpanded = !isExpanded }, ) identityState.identityName?.let { identityName -> - item { + item(key = "identityName") { IdentityCopyField( label = stringResource(id = R.string.identity_name), value = identityName, @@ -76,12 +76,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, @@ -97,12 +98,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, @@ -118,12 +120,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, @@ -139,12 +142,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, @@ -160,12 +164,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, @@ -181,12 +186,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, @@ -202,12 +208,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, @@ -223,12 +230,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, @@ -244,19 +252,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( @@ -269,23 +279,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, @@ -295,29 +310,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, @@ -326,7 +347,7 @@ fun VaultItemIdentityContent( } } - item { + item(key = "lastUpdated") { Spacer(modifier = Modifier.height(height = 16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", @@ -334,7 +355,8 @@ fun VaultItemIdentityContent( modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 12.dp), + .padding(horizontal = 12.dp) + .animateItem(), ) } item { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index fea1811028..ee43beb1e3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -72,21 +72,22 @@ fun VaultItemLoginContent( 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, @@ -96,13 +97,14 @@ fun VaultItemLoginContent( ?: CardStyle.Full, modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .animateItem(), ) } } loginItemState.passwordData?.let { passwordData -> - item { + item(key = "passwordData") { PasswordField( passwordData = passwordData, onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick, @@ -114,25 +116,27 @@ fun VaultItemLoginContent( ?: CardStyle.Full, modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .animateItem(), ) } } loginItemState.fido2CredentialCreationDateText?.let { creationDate -> - item { + item(key = "creationDate") { Spacer(modifier = Modifier.height(8.dp)) Fido2CredentialField( creationDate = creationDate(), modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .animateItem(), ) } } loginItemState.totpCodeItemData?.let { totpCodeItemData -> - item { + item(key = "totpCode") { Spacer(modifier = Modifier.height(8.dp)) TotpField( totpCodeItemData = totpCodeItemData, @@ -142,25 +146,30 @@ fun VaultItemLoginContent( .onAuthenticatorHelpToolTipClick, modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .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, @@ -168,20 +177,22 @@ fun VaultItemLoginContent( cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp), modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .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 .standardHorizontalMargin() .padding(horizontal = 16.dp) - .fillMaxWidth(), + .fillMaxWidth() + .animateItem(), ) Spacer(modifier = Modifier.height(8.dp)) NotesField( @@ -189,57 +200,68 @@ fun VaultItemLoginContent( onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick, modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .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, onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, + vaultCommonItemTypeHandlers.onCopyCustomTextField, onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, cardStyle = CardStyle.Full, modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .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 .standardHorizontalMargin() .padding(horizontal = 16.dp) - .fillMaxWidth(), + .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(), + .fillMaxWidth() + .animateItem(), attachmentItem = attachmentItem, cardStyle = attachments.toListItemCardStyle(index = index), onAttachmentDownloadClick = vaultCommonItemTypeHandlers @@ -248,7 +270,7 @@ fun VaultItemLoginContent( } } - item { + item(key = "lastUpdated") { Spacer(modifier = Modifier.height(16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", @@ -256,12 +278,13 @@ fun VaultItemLoginContent( modifier = Modifier .standardHorizontalMargin() .padding(horizontal = 12.dp) - .fillMaxWidth(), + .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)}: ", @@ -269,13 +292,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, @@ -287,7 +311,8 @@ fun VaultItemLoginContent( modifier = Modifier .wrapContentWidth() .standardHorizontalMargin() - .padding(horizontal = 12.dp), + .padding(horizontal = 12.dp) + .animateItem(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index ea32ae974f..99039af9bc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -7,7 +7,6 @@ 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 @@ -58,7 +57,7 @@ fun VaultItemSecureNoteContent( onExpandClick = { isExpanded = !isExpanded }, ) commonState.notes?.let { notes -> - item { + item(key = "notes") { Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.notes), @@ -78,59 +77,70 @@ 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, onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, + vaultCommonItemTypeHandlers.onCopyCustomTextField, onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, 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, @@ -139,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)}: ", diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt index 729a043a2f..f9dc346f1e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt @@ -6,7 +6,6 @@ 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 @@ -58,7 +57,7 @@ fun VaultItemSshKeyContent( isExpanded = isExpanded, onExpandClick = { isExpanded = !isExpanded }, ) - item { + item(key = "publicKey") { Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.public_key), @@ -78,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, @@ -104,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, @@ -127,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( @@ -160,57 +163,68 @@ 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, onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + vaultCommonItemTypeHandlers.onCopyCustomHiddenField, onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, + vaultCommonItemTypeHandlers.onCopyCustomTextField, onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + vaultCommonItemTypeHandlers.onShowHiddenFieldClick, 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, @@ -219,7 +233,7 @@ fun VaultItemSshKeyContent( } } - item { + item(key = "lastUpdated") { Spacer(modifier = Modifier.height(height = 16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", @@ -228,6 +242,7 @@ fun VaultItemSshKeyContent( .fillMaxWidth() .standardHorizontalMargin() .padding(horizontal = 12.dp) + .animateItem() .testTag("SshKeyItemLastUpdated"), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index ed3d999357..ad1f8d1fcb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -46,7 +46,7 @@ private const val EXPANDABLE_THRESHOLD = 2 /** * Reusable composable for displaying the cipher name, favorite status, and related locations. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") fun LazyListScope.itemHeader( value: String, isFavorite: Boolean, @@ -256,9 +256,9 @@ private fun LazyItemScope.ItemLocationListItem( } //region Previews -//@Composable -//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -//private fun ItemHeader_LocalIcon_Preview() { +// @Composable +// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +// private fun ItemHeader_LocalIcon_Preview() { // BitwardenTheme { // ItemHeader( // value = "Login without favicon", @@ -269,11 +269,11 @@ private fun LazyItemScope.ItemLocationListItem( // relatedLocations = persistentListOf(), // ) // } -//} +// } // -//@Composable -//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -//private fun ItemHeader_NetworkIcon_Preview() { +// @Composable +// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +// private fun ItemHeader_NetworkIcon_Preview() { // BitwardenTheme { // ItemHeader( // value = "Login with favicon", @@ -285,11 +285,11 @@ private fun LazyItemScope.ItemLocationListItem( // relatedLocations = persistentListOf(), // ) // } -//} +// } // -//@Composable -//@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -//private fun ItemHeader_Organization_Preview() { +// @Composable +// @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +// private fun ItemHeader_Organization_Preview() { // BitwardenTheme { // ItemHeader( // value = "Login without favicon", @@ -302,11 +302,11 @@ private fun LazyItemScope.ItemLocationListItem( // ), // ) // } -//} +// } // -//@Composable -//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -//private fun ItemNameField_Org_SingleCollection_Preview() { +// @Composable +// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +// private fun ItemNameField_Org_SingleCollection_Preview() { // BitwardenTheme { // ItemHeader( // value = "Login without favicon", @@ -320,11 +320,11 @@ private fun LazyItemScope.ItemLocationListItem( // ), // ) // } -//} +// } // -//@Composable -//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -//private fun ItemNameField_Org_MultiCollection_Preview() { +// @Composable +// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +// private fun ItemNameField_Org_MultiCollection_Preview() { // BitwardenTheme { // ItemHeader( // value = "Login without favicon", @@ -339,11 +339,11 @@ private fun LazyItemScope.ItemLocationListItem( // ), // ) // } -//} +// } // -//@Composable -//@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -//private fun ItemNameField_Org_SingleCollection_Folder_Preview() { +// @Composable +// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +// private fun ItemNameField_Org_SingleCollection_Folder_Preview() { // BitwardenTheme { // ItemHeader( // value = "Note without favicon", @@ -358,5 +358,5 @@ private fun LazyItemScope.ItemLocationListItem( // ), // ) // } -//} +// } //endregion Previews diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 24aaefef43..816324e229 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -3341,7 +3341,6 @@ class VaultItemViewModelTest : BaseViewModelTest() { shouldManageResetPassword = false, shouldUseKeyConnector = false, role = OrganizationType.OWNER, - shouldManagePolicies = false, ), ), ), From fcfcf48cee462a739cfaa8a198735ecbb9dc4e05 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Thu, 27 Feb 2025 09:17:50 -0500 Subject: [PATCH 22/27] Uncomment & update previews --- .../feature/item/VaultItemCardContent.kt | 9 +- .../feature/item/component/ItemHeader.kt | 243 ++++++++++-------- 2 files changed, 143 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index 0c0f3db4a1..5ca29b5814 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -253,12 +253,9 @@ fun VaultItemCardContent( Spacer(modifier = Modifier.height(height = 8.dp)) CustomField( customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index ad1f8d1fcb..eaeb83e9b4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -10,12 +10,17 @@ 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.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 @@ -23,6 +28,7 @@ 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 @@ -37,6 +43,7 @@ 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. @@ -256,107 +263,137 @@ private fun LazyItemScope.ItemLocationListItem( } //region Previews -// @Composable -// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -// private fun ItemHeader_LocalIcon_Preview() { -// BitwardenTheme { -// ItemHeader( -// value = "Login without favicon", -// isFavorite = true, -// iconData = IconData.Local( -// iconRes = R.drawable.ic_globe, -// ), -// relatedLocations = persistentListOf(), -// ) -// } -// } -// -// @Composable -// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -// private fun ItemHeader_NetworkIcon_Preview() { -// BitwardenTheme { -// ItemHeader( -// value = "Login with favicon", -// isFavorite = true, -// iconData = IconData.Network( -// uri = "mockuri", -// fallbackIconRes = R.drawable.ic_globe, -// ), -// relatedLocations = persistentListOf(), -// ) -// } -// } -// -// @Composable -// @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -// private fun ItemHeader_Organization_Preview() { -// BitwardenTheme { -// ItemHeader( -// value = "Login without favicon", -// isFavorite = true, -// iconData = IconData.Local( -// iconRes = R.drawable.ic_globe, -// ), -// relatedLocations = persistentListOf( -// VaultItemLocation.Organization("Stark Industries"), -// ), -// ) -// } -// } -// -// @Composable -// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -// private fun ItemNameField_Org_SingleCollection_Preview() { -// BitwardenTheme { -// ItemHeader( -// value = "Login without favicon", -// isFavorite = true, -// iconData = IconData.Local( -// iconRes = R.drawable.ic_globe, -// ), -// relatedLocations = persistentListOf( -// VaultItemLocation.Organization("Stark Industries"), -// VaultItemLocation.Collection("Marketing"), -// ), -// ) -// } -// } -// -// @Composable -// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -// private fun ItemNameField_Org_MultiCollection_Preview() { -// BitwardenTheme { -// 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"), -// ), -// ) -// } -// } -// -// @Composable -// @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -// private fun ItemNameField_Org_SingleCollection_Folder_Preview() { -// BitwardenTheme { -// 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"), -// ), -// ) -// } -// } +@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 From 27043e28a8f709073e85636828ed008062e63843 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Thu, 27 Feb 2025 10:30:41 -0500 Subject: [PATCH 23/27] Revert formatting changes --- .../ui/vault/feature/item/VaultItemLoginContent.kt | 10 +++------- .../vault/feature/item/VaultItemSecureNoteContent.kt | 9 +++------ .../ui/vault/feature/item/VaultItemSshKeyContent.kt | 9 +++------ 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index ee43beb1e3..267a3927ca 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -8,7 +8,6 @@ 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 @@ -225,12 +224,9 @@ fun VaultItemLoginContent( Spacer(modifier = Modifier.height(height = 8.dp)) CustomField( customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, cardStyle = CardStyle.Full, modifier = Modifier .standardHorizontalMargin() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index 99039af9bc..a90d43d2d6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -103,12 +103,9 @@ fun VaultItemSecureNoteContent( Spacer(modifier = Modifier.height(height = 8.dp)) CustomField( customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt index f9dc346f1e..739a96b22a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt @@ -188,12 +188,9 @@ fun VaultItemSshKeyContent( Spacer(modifier = Modifier.height(height = 8.dp)) CustomField( customField = customField, - onCopyCustomHiddenField = - vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = - vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = - vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() From b3219d40403d8829352d1e2c55cb057c6cdf87bd Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Thu, 27 Feb 2025 11:05:06 -0500 Subject: [PATCH 24/27] Use CardStyle.Bottom when last item is drawn --- .../vault/feature/item/component/ItemHeader.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index eaeb83e9b4..547f71d40c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -14,6 +14,7 @@ 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 @@ -128,23 +129,23 @@ fun LazyListScope.itemHeader( return } - items( - key = { "locations_$it" }, + itemsIndexed( + key = { index, _ -> "locations_$index" }, items = relatedLocations.take(EXPANDABLE_THRESHOLD), - ) { + ) { index, location -> ItemLocationListItem( - vectorPainter = rememberVectorPainter(it.icon), + vectorPainter = rememberVectorPainter(location.icon), iconTestTag = "ItemLocationIcon", - text = it.name, + text = location.name, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin() .animateItem() .cardStyle( - cardStyle = if (relatedLocations.size > EXPANDABLE_THRESHOLD) { - CardStyle.Middle(hasDivider = false) - } else { + cardStyle = if (index == relatedLocations.size - 1) { CardStyle.Bottom + } else { + CardStyle.Middle(hasDivider = false) }, paddingVertical = 0.dp, paddingHorizontal = 16.dp, From a24c6f27192f5b53eab88aa60c09701b68b1df8f Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Thu, 27 Feb 2025 11:13:04 -0500 Subject: [PATCH 25/27] Update `ic_organization.xml` vector drawable --- app/src/main/res/drawable/ic_organization.xml | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/drawable/ic_organization.xml b/app/src/main/res/drawable/ic_organization.xml index e84e4015f8..f6a105f7d0 100644 --- a/app/src/main/res/drawable/ic_organization.xml +++ b/app/src/main/res/drawable/ic_organization.xml @@ -1,28 +1,28 @@ + android:width="24dp" + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> + 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"/> + 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"/> + 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"/> + 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"/> + 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"/> + 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"/> From b6163cf53ce38dcad067963f1e10e56f452a8ff0 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Thu, 27 Feb 2025 11:14:40 -0500 Subject: [PATCH 26/27] Fix item key in `VaultItemCardContent` for security code field --- .../bitwarden/ui/vault/feature/item/VaultItemCardContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index 5ca29b5814..ea709ec9b0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -163,7 +163,7 @@ fun VaultItemCardContent( } cardState.securityCode?.let { securityCodeData -> - item(key = "securityCodeData_$securityCodeData") { + item(key = "securityCode") { BitwardenPasswordField( label = stringResource(id = R.string.security_code), value = securityCodeData.code, From e8a98dd3ed1db0004510a01bafa949fdf9b9be92 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Thu, 27 Feb 2025 12:05:09 -0500 Subject: [PATCH 27/27] Add spacers to VaultItemIdentityContent, VaultItemSecureNoteContent, and VaultItemCardContent --- .../bitwarden/ui/vault/feature/item/VaultItemCardContent.kt | 3 +++ .../ui/vault/feature/item/VaultItemIdentityContent.kt | 3 +++ .../ui/vault/feature/item/VaultItemSecureNoteContent.kt | 3 +++ .../bitwarden/ui/vault/feature/item/component/ItemHeader.kt | 4 +++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index ea709ec9b0..3c86522aac 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -60,6 +60,9 @@ fun VaultItemCardContent( isExpanded = isExpanded, onExpandClick = { isExpanded = !isExpanded }, ) + item { + Spacer(modifier = Modifier.height(height = 8.dp)) + } cardState.cardholderName?.let { cardholderName -> item(key = "cardholderName") { BitwardenTextField( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index 72069334f8..dd8fb3c0d3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -59,6 +59,9 @@ fun VaultItemIdentityContent( isExpanded = isExpanded, onExpandClick = { isExpanded = !isExpanded }, ) + item { + Spacer(modifier = Modifier.height(height = 8.dp)) + } identityState.identityName?.let { identityName -> item(key = "identityName") { IdentityCopyField( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index a90d43d2d6..fddcdc6de2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -56,6 +56,9 @@ fun VaultItemSecureNoteContent( isExpanded = isExpanded, onExpandClick = { isExpanded = !isExpanded }, ) + item { + Spacer(modifier = Modifier.height(height = 8.dp)) + } commonState.notes?.let { notes -> item(key = "notes") { Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt index 547f71d40c..1c793b32c7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/component/ItemHeader.kt @@ -112,7 +112,7 @@ fun LazyListScope.itemHeader( } if (relatedLocations.isEmpty()) { - item { + item(key = "noFolder") { ItemLocationListItem( vectorPainter = rememberVectorPainter(R.drawable.ic_folder), text = stringResource(R.string.no_folder), @@ -120,9 +120,11 @@ fun LazyListScope.itemHeader( modifier = Modifier .standardHorizontalMargin() .fillMaxWidth() + .animateItem() .cardStyle( cardStyle = CardStyle.Bottom, paddingVertical = 0.dp, + paddingHorizontal = 16.dp, ), ) }