From 1c8501b69bf5c419320cd33932797f6d68b8b7f9 Mon Sep 17 00:00:00 2001 From: Oleg Semenenko <146032743+oleg-livefront@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:39:18 -0600 Subject: [PATCH] BIT-515, BIT-512 Adding the ability to view and edit secure note items. (#462) --- .../ui/vault/feature/item/VaultItemScreen.kt | 6 +- .../item/VaultItemSecureNoteContent.kt | 132 ++++++++++++++++++ .../vault/feature/item/VaultItemViewModel.kt | 2 +- .../vault/feature/item/VaultItemScreenTest.kt | 14 ++ .../item/util/CipherViewExtensionsTest.kt | 29 ++++ 5 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt index d7245553a..d430ac467 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt @@ -217,7 +217,11 @@ private fun VaultItemContent( } is VaultItemState.ViewState.Content.ItemType.SecureNote -> { - // TODO UI for viewing SecureNote BIT-515 + VaultItemSecureNoteContent( + commonState = viewState.common, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + modifier = modifier, + ) } } } 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 new file mode 100644 index 000000000..54d9935d3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -0,0 +1,132 @@ +package com.x8bit.bitwarden.ui.vault.feature.item + +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.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography +import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers + +/** + * The top level content UI state for the [VaultItemScreen] when viewing a secure note cipher. + */ +@Suppress("LongMethod") +@Composable +fun VaultItemSecureNoteContent( + commonState: VaultItemState.ViewState.Content.Common, + vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + item { + BitwardenListHeaderText( + label = stringResource(id = R.string.item_information), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + item { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.name), + value = commonState.name, + onValueChange = { }, + readOnly = true, + singleLine = false, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + commonState.notes?.let { notes -> + item { + Spacer(modifier = Modifier.height(4.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.notes), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + + commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> + item { + Spacer(modifier = Modifier.height(4.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.custom_fields), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + items(customFields) { customField -> + Spacer(modifier = Modifier.height(8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + + item { + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .semantics(mergeDescendants = true) { }, + ) { + Text( + text = "${stringResource(id = R.string.date_updated)}: ", + style = LocalNonMaterialTypography.current.labelMediumProminent, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = commonState.lastUpdated, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + 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/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index aac8a4c88..294832f7e 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 @@ -768,7 +768,7 @@ sealed class VaultItemAction { ) : Common() /** - * The user has clicked to display the a hidden field. + * The user has clicked to display the hidden field. */ data class HiddenFieldVisibilityClicked( val field: VaultItemState.ViewState.Content.Common.Custom.HiddenField, 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 2d53c8078..90dbd6fec 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 @@ -1164,6 +1164,12 @@ private val EMPTY_IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content = type = EMPTY_IDENTITY_TYPE, ) +private val EMPTY_SECURE_NOTE_VIEW_STATE = + VaultItemState.ViewState.Content( + common = EMPTY_COMMON, + type = VaultItemState.ViewState.Content.ItemType.SecureNote, + ) + private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content = VaultItemState.ViewState.Content( type = DEFAULT_LOGIN, @@ -1176,12 +1182,20 @@ private val DEFAULT_IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content = common = DEFAULT_COMMON, ) +private val DEFAULT_SECURE_NOTE_VIEW_STATE: VaultItemState.ViewState.Content = + VaultItemState.ViewState.Content( + common = DEFAULT_COMMON, + type = VaultItemState.ViewState.Content.ItemType.SecureNote, + ) + private val EMPTY_VIEW_STATES = listOf( EMPTY_LOGIN_VIEW_STATE, EMPTY_IDENTITY_VIEW_STATE, + EMPTY_SECURE_NOTE_VIEW_STATE, ) private val DEFAULT_VIEW_STATES = listOf( DEFAULT_LOGIN_VIEW_STATE, DEFAULT_IDENTITY_VIEW_STATE, + DEFAULT_SECURE_NOTE_VIEW_STATE, ) 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 f0eef65fe..2b1f2f590 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 @@ -170,4 +170,33 @@ class CipherViewExtensionsTest { result, ) } + + @Suppress("MaxLineLength") + @Test + fun `toViewState should transform full CipherView into ViewState Secure Note Content with premium`() { + val viewState = createCipherView(type = CipherType.SECURE_NOTE, isEmpty = false) + .toViewState(isPremiumUser = true) + + assertEquals( + VaultItemState.ViewState.Content( + common = createCommonContent(isEmpty = false), + type = VaultItemState.ViewState.Content.ItemType.SecureNote, + ), + viewState, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `toViewState should transform empty Secure Note CipherView into ViewState Secure Note Content`() { + val viewState = createCipherView(type = CipherType.SECURE_NOTE, isEmpty = true) + .toViewState(isPremiumUser = true) + + val expectedState = VaultItemState.ViewState.Content( + common = createCommonContent(isEmpty = true).copy(isPremiumUser = true), + type = VaultItemState.ViewState.Content.ItemType.SecureNote, + ) + + assertEquals(expectedState, viewState) + } }