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 29d25f365..fd2a182d7 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
@@ -16,6 +16,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
+import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
@@ -173,14 +174,22 @@ fun VaultItemCardContent(
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ BitwardenTextFieldWithActions(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
+ actions = {
+ BitwardenTonalIconButton(
+ vectorIconRes = R.drawable.ic_copy,
+ contentDescription = stringResource(id = R.string.copy_notes),
+ onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
+ modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
+ )
+ },
+ textFieldTestTag = "CipherNotesLabel",
modifier = Modifier
- .testTag("CipherNotesLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
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 6606a4bae..3e82215ff 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
@@ -13,19 +13,23 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
+import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
+import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
/**
* The top level content UI state for the [VaultItemScreen] when viewing a Identity cipher.
*/
-@Suppress("LongMethod")
+@Suppress("LongMethod", "MaxLineLength")
@Composable
fun VaultItemIdentityContent(
identityState: VaultItemState.ViewState.Content.ItemType.Identity,
commonState: VaultItemState.ViewState.Content.Common,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
+ vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
@@ -54,14 +58,14 @@ fun VaultItemIdentityContent(
identityState.identityName?.let { identityName ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.identity_name),
value = identityName,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_identity_name),
+ textFieldTestTag = "IdentityNameEntry",
+ copyActionTestTag = "IdentityCopyNameButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick,
modifier = Modifier
- .testTag("IdentityNameEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -70,14 +74,14 @@ fun VaultItemIdentityContent(
identityState.username?.let { username ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.username),
value = username,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_username),
+ textFieldTestTag = "IdentityUsernameEntry",
+ copyActionTestTag = "IdentityCopyUsernameButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick,
modifier = Modifier
- .testTag("IdentityUsernameEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -86,14 +90,14 @@ fun VaultItemIdentityContent(
identityState.company?.let { company ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.company),
value = company,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_company),
+ textFieldTestTag = "IdentityCompanyEntry",
+ copyActionTestTag = "IdentityCopyCompanyButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick,
modifier = Modifier
- .testTag("IdentityCompanyEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -102,14 +106,14 @@ fun VaultItemIdentityContent(
identityState.ssn?.let { ssn ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.ssn),
value = ssn,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_ssn),
+ textFieldTestTag = "IdentitySsnEntry",
+ copyActionTestTag = "IdentityCopySsnButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick,
modifier = Modifier
- .testTag("IdentitySsnEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -118,14 +122,14 @@ fun VaultItemIdentityContent(
identityState.passportNumber?.let { passportNumber ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.passport_number),
value = passportNumber,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_passport_number),
+ textFieldTestTag = "IdentityPassportNumberEntry",
+ copyActionTestTag = "IdentityCopyPassportNumberButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick,
modifier = Modifier
- .testTag("IdentityPassportNumberEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -134,14 +138,14 @@ fun VaultItemIdentityContent(
identityState.licenseNumber?.let { licenseNumber ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.license_number),
value = licenseNumber,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_license_number),
+ textFieldTestTag = "IdentityLicenseNumberEntry",
+ copyActionTestTag = "IdentityCopyLicenseNumberButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick,
modifier = Modifier
- .testTag("IdentityLicenseNumberEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -150,14 +154,14 @@ fun VaultItemIdentityContent(
identityState.email?.let { email ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.email),
value = email,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_email),
+ textFieldTestTag = "IdentityEmailEntry",
+ copyActionTestTag = "IdentityCopyEmailButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick,
modifier = Modifier
- .testTag("IdentityEmailEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -166,14 +170,14 @@ fun VaultItemIdentityContent(
identityState.phone?.let { phone ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.phone),
value = phone,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_phone),
+ textFieldTestTag = "IdentityPhoneEntry",
+ copyActionTestTag = "IdentityCopyPhoneButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick,
modifier = Modifier
- .testTag("IdentityPhoneEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -182,20 +186,19 @@ fun VaultItemIdentityContent(
identityState.address?.let { address ->
item {
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.address),
value = address,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_address),
+ textFieldTestTag = "IdentityAddressEntry",
+ copyActionTestTag = "IdentityCopyAddressButton",
+ onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick,
modifier = Modifier
- .testTag("IdentityAddressEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
-
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(4.dp))
@@ -206,14 +209,14 @@ fun VaultItemIdentityContent(
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ IdentityCopyField(
label = stringResource(id = R.string.notes),
value = notes,
- onValueChange = { },
- readOnly = true,
- singleLine = false,
+ copyContentDescription = stringResource(id = R.string.copy_notes),
+ textFieldTestTag = "CipherNotesLabel",
+ copyActionTestTag = "CipherNotesCopyButton",
+ onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier
- .testTag("CipherNotesLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -284,3 +287,32 @@ fun VaultItemIdentityContent(
}
}
}
+
+@Composable
+private fun IdentityCopyField(
+ label: String,
+ value: String,
+ copyContentDescription: String,
+ textFieldTestTag: String,
+ copyActionTestTag: String,
+ onCopyClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BitwardenTextFieldWithActions(
+ label = label,
+ value = value,
+ onValueChange = { },
+ readOnly = true,
+ singleLine = false,
+ actions = {
+ BitwardenTonalIconButton(
+ vectorIconRes = R.drawable.ic_copy,
+ contentDescription = copyContentDescription,
+ onClick = onCopyClick,
+ modifier = Modifier.testTag(tag = copyActionTestTag),
+ )
+ },
+ modifier = modifier,
+ textFieldTestTag = textFieldTestTag,
+ )
+}
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 308741fb3..fce007507 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
@@ -158,8 +158,8 @@ fun VaultItemLoginContent(
Spacer(modifier = Modifier.height(8.dp))
NotesField(
notes = notes,
+ onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier
- .testTag("CipherNotesLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
@@ -273,14 +273,24 @@ private fun Fido2CredentialField(
@Composable
private fun NotesField(
notes: String,
+ onCopyAction: () -> Unit,
modifier: Modifier = Modifier,
) {
- BitwardenTextField(
+ BitwardenTextFieldWithActions(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
+ actions = {
+ BitwardenTonalIconButton(
+ vectorIconRes = R.drawable.ic_copy,
+ contentDescription = stringResource(id = R.string.copy_notes),
+ onClick = onCopyAction,
+ modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
+ )
+ },
+ textFieldTestTag = "CipherNotesLabel",
modifier = modifier,
)
}
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 5f8ca044e..268ac415a 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
@@ -44,6 +44,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
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.feature.item.handlers.VaultIdentityItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
@@ -272,6 +273,9 @@ fun VaultItemScreen(
vaultSshKeyItemTypeHandlers = remember(viewModel) {
VaultSshKeyItemTypeHandlers.create(viewModel = viewModel)
},
+ vaultIdentityItemTypeHandlers = remember(viewModel) {
+ VaultIdentityItemTypeHandlers.create(viewModel = viewModel)
+ },
)
}
}
@@ -350,6 +354,7 @@ private fun VaultItemContent(
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
+ vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
modifier: Modifier = Modifier,
) {
when (viewState) {
@@ -386,6 +391,7 @@ private fun VaultItemContent(
commonState = viewState.common,
identityState = viewState.type,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
+ vaultIdentityItemTypeHandlers = vaultIdentityItemTypeHandlers,
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
index 7fbbdfc64..9d68e3dc5 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
@@ -16,7 +16,9 @@ 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.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
+import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
@@ -66,14 +68,22 @@ fun VaultItemSecureNoteContent(
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
+ BitwardenTextFieldWithActions(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
+ actions = {
+ BitwardenTonalIconButton(
+ vectorIconRes = R.drawable.ic_copy,
+ contentDescription = stringResource(id = R.string.copy_notes),
+ onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
+ modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
+ )
+ },
+ textFieldTestTag = "CipherNotesLabel",
modifier = Modifier
- .testTag("CipherNotesLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
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 f119ce545..021d88624 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
@@ -133,6 +133,7 @@ class VaultItemViewModel @Inject constructor(
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action)
+ is VaultItemAction.ItemType.Identity -> handleIdentityTypeActions(action)
is VaultItemAction.Common -> handleCommonActions(action)
is VaultItemAction.Internal -> handleInternalAction(action)
}
@@ -184,6 +185,7 @@ class VaultItemViewModel @Inject constructor(
}
is VaultItemAction.Common.RestoreVaultItemClick -> handleRestoreItemClicked()
+ is VaultItemAction.Common.CopyNotesClick -> handleCopyNotesClick()
}
}
@@ -508,6 +510,13 @@ class VaultItemViewModel @Inject constructor(
}
}
+ private fun handleCopyNotesClick() {
+ onContent { content ->
+ val notes = content.common.notes.orEmpty()
+ clipboardManager.setText(text = notes)
+ }
+ }
+
//endregion Common Handlers
//region Login Type Handlers
@@ -812,6 +821,99 @@ class VaultItemViewModel @Inject constructor(
//endregion SSH Key Type Handlers
+ //region Identity Type Handlers
+
+ private fun handleIdentityTypeActions(action: VaultItemAction.ItemType.Identity) {
+ when (action) {
+ VaultItemAction.ItemType.Identity.CopyIdentityNameClick -> {
+ handleCopyIdentityNameClick()
+ }
+
+ VaultItemAction.ItemType.Identity.CopyUsernameClick -> {
+ handleCopyIdentityUsernameClick()
+ }
+
+ VaultItemAction.ItemType.Identity.CopyCompanyClick -> handleCopyCompanyClick()
+ VaultItemAction.ItemType.Identity.CopySsnClick -> handleCopySsnClick()
+ VaultItemAction.ItemType.Identity.CopyPassportNumberClick -> {
+ handleCopyPassportNumberClick()
+ }
+
+ VaultItemAction.ItemType.Identity.CopyLicenseNumberClick -> {
+ handleCopyLicenseNumberClick()
+ }
+
+ VaultItemAction.ItemType.Identity.CopyEmailClick -> handleCopyEmailClick()
+ VaultItemAction.ItemType.Identity.CopyPhoneClick -> handleCopyPhoneClick()
+ VaultItemAction.ItemType.Identity.CopyAddressClick -> handleCopyAddressClick()
+ }
+ }
+
+ private fun handleCopyIdentityNameClick() {
+ onIdentityContent { _, identity ->
+ val identityName = identity.identityName.orEmpty()
+ clipboardManager.setText(text = identityName)
+ }
+ }
+
+ private fun handleCopyIdentityUsernameClick() {
+ onIdentityContent { _, identity ->
+ val username = identity.username.orEmpty()
+ clipboardManager.setText(text = username)
+ }
+ }
+
+ private fun handleCopyCompanyClick() {
+ onIdentityContent { _, identity ->
+ val company = identity.company.orEmpty()
+ clipboardManager.setText(text = company)
+ }
+ }
+
+ private fun handleCopySsnClick() {
+ onIdentityContent { _, identity ->
+ val ssn = identity.ssn.orEmpty()
+ clipboardManager.setText(text = ssn)
+ }
+ }
+
+ private fun handleCopyPassportNumberClick() {
+ onIdentityContent { _, identity ->
+ val passportNumber = identity.passportNumber.orEmpty()
+ clipboardManager.setText(text = passportNumber)
+ }
+ }
+
+ private fun handleCopyLicenseNumberClick() {
+ onIdentityContent { _, identity ->
+ val licenseNumber = identity.licenseNumber.orEmpty()
+ clipboardManager.setText(text = licenseNumber)
+ }
+ }
+
+ private fun handleCopyEmailClick() {
+ onIdentityContent { _, identity ->
+ val email = identity.email.orEmpty()
+ clipboardManager.setText(text = email)
+ }
+ }
+
+ private fun handleCopyPhoneClick() {
+ onIdentityContent { _, identity ->
+ val phone = identity.phone.orEmpty()
+ clipboardManager.setText(text = phone)
+ }
+ }
+
+ private fun handleCopyAddressClick() {
+ onIdentityContent { _, identity ->
+ val address = identity.address.orEmpty()
+ clipboardManager.setText(text = address)
+ }
+ }
+
+ //endregion Identity Type Handlers
+
//region Internal Type Handlers
private fun handleInternalAction(action: VaultItemAction.Internal) {
@@ -1133,6 +1235,21 @@ class VaultItemViewModel @Inject constructor(
}
}
}
+
+ private inline fun onIdentityContent(
+ crossinline block: (
+ VaultItemState.ViewState.Content,
+ VaultItemState.ViewState.Content.ItemType.Identity,
+ ) -> Unit,
+ ) {
+ state.viewState.asContentOrNull()
+ ?.let { content ->
+ (content.type as? VaultItemState.ViewState.Content.ItemType.Identity)
+ ?.let { identityContent ->
+ block(content, identityContent)
+ }
+ }
+ }
}
/**
@@ -1724,6 +1841,11 @@ sealed class VaultItemAction {
* The user confirmed cloning a cipher without its FIDO 2 credentials.
*/
data object ConfirmCloneWithoutFido2CredentialClick : Common()
+
+ /**
+ * The user has clicked the copy button for notes text field.
+ */
+ data object CopyNotesClick : Common()
}
/**
@@ -1827,6 +1949,56 @@ sealed class VaultItemAction {
*/
data object CopyFingerprintClick : SshKey()
}
+
+ /**
+ * Represents actions specific to the Identity type.
+ */
+ sealed class Identity : VaultItemAction() {
+ /**
+ * The user has clicked the copy button for the identity name.
+ */
+ data object CopyIdentityNameClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the username.
+ */
+ data object CopyUsernameClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the company.
+ */
+ data object CopyCompanyClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the SSN.
+ */
+ data object CopySsnClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the passport number.
+ */
+ data object CopyPassportNumberClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the license number.
+ */
+ data object CopyLicenseNumberClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the email.
+ */
+ data object CopyEmailClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the phone number.
+ */
+ data object CopyPhoneClick : Identity()
+
+ /**
+ * The user has clicked the copy button for the address.
+ */
+ data object CopyAddressClick : Identity()
+ }
}
/**
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt
index ec011346e..7dfa682ca 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt
@@ -21,6 +21,7 @@ data class VaultCommonItemTypeHandlers(
Boolean,
) -> Unit,
val onAttachmentDownloadClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit,
+ val onCopyNotesClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
@@ -52,6 +53,9 @@ data class VaultCommonItemTypeHandlers(
onAttachmentDownloadClick = {
viewModel.trySendAction(VaultItemAction.Common.AttachmentDownloadClick(it))
},
+ onCopyNotesClick = {
+ viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
+ },
)
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultIdentityItemTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultIdentityItemTypeHandlers.kt
new file mode 100644
index 000000000..d44ac6066
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultIdentityItemTypeHandlers.kt
@@ -0,0 +1,59 @@
+package com.x8bit.bitwarden.ui.vault.feature.item.handlers
+
+import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
+import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
+
+/**
+ * A collection of handler functions for managing actions within the context of viewing identity
+ * items in a vault.
+ */
+data class VaultIdentityItemTypeHandlers(
+ val onCopyIdentityNameClick: () -> Unit,
+ val onCopyUsernameClick: () -> Unit,
+ val onCopyCompanyClick: () -> Unit,
+ val onCopySsnClick: () -> Unit,
+ val onCopyPassportNumberClick: () -> Unit,
+ val onCopyLicenseNumberClick: () -> Unit,
+ val onCopyEmailClick: () -> Unit,
+ val onCopyPhoneClick: () -> Unit,
+ val onCopyAddressClick: () -> Unit,
+) {
+ @Suppress("UndocumentedPublicClass", "MaxLineLength")
+ companion object {
+ /**
+ * Creates the [VaultIdentityItemTypeHandlers] using the [viewModel] to send desired actions.
+ */
+ fun create(
+ viewModel: VaultItemViewModel,
+ ): VaultIdentityItemTypeHandlers =
+ VaultIdentityItemTypeHandlers(
+ onCopyIdentityNameClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick)
+ },
+ onCopyUsernameClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyUsernameClick)
+ },
+ onCopyCompanyClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyCompanyClick)
+ },
+ onCopySsnClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopySsnClick)
+ },
+ onCopyPassportNumberClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick)
+ },
+ onCopyLicenseNumberClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyLicenseNumberClick)
+ },
+ onCopyEmailClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyEmailClick)
+ },
+ onCopyPhoneClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick)
+ },
+ onCopyAddressClick = {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyAddressClick)
+ },
+ )
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a6f12dfda..426bcbcce 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1087,4 +1087,12 @@ Do you want to switch to this account?
Skip for now
Done
%1$s of %2$s
+ Copy identity name
+ Copy company
+ Copy social security number
+ Copy passport number
+ Copy license number
+ Copy email
+ Copy phone number
+ Copy address
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 0c71c774f..f63b90561 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
@@ -19,6 +19,7 @@ import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onSiblings
import androidx.compose.ui.test.performClick
@@ -1257,6 +1258,114 @@ class VaultItemScreenTest : BaseComposeTest() {
.filterToOne(hasAnyAncestor(isPopup()))
.assertDoesNotExist()
}
+
+ @Test
+ fun `on login copy notes field click should send CopyNotesClick`() {
+
+ mutableStateFlow.update { currentState ->
+ currentState.copy(
+ viewState = DEFAULT_LOGIN_VIEW_STATE,
+ )
+ }
+ composeTestRule.onNodeWithTextAfterScroll("Lots of notes")
+ composeTestRule
+ .onNodeWithTag("CipherNotesCopyButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
+ }
+ }
+
+ @Test
+ fun `on identity copy notes field click should send CopyNotesClick`() {
+ // Adding a custom field so that we can scroll to it
+ // So we can see the Copy notes button but not have it covered by the FAB
+ val textField = VaultItemState.ViewState.Content.Common.Custom.TextField(
+ name = "text",
+ value = "value",
+ isCopyable = true,
+ )
+
+ EMPTY_VIEW_STATES
+ .forEach { typeState ->
+ mutableStateFlow.update { currentState ->
+ currentState.copy(
+ viewState = typeState.copy(
+ type = DEFAULT_IDENTITY,
+ common = EMPTY_COMMON.copy(
+ notes = "this is a note",
+ customFields = listOf(textField),
+ ),
+ ),
+ )
+ }
+
+ composeTestRule.onNodeWithTextAfterScroll(textField.name)
+ composeTestRule
+ .onNodeWithTag("CipherNotesCopyButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
+ }
+ }
+ }
+
+ @Test
+ fun `on card copy notes field click should send CopyNotesClick`() {
+ // Adding a custom field so that we can scroll to it
+ // So we can see the Copy notes button but not have it covered by the FAB
+ val textField = VaultItemState.ViewState.Content.Common.Custom.TextField(
+ name = "text",
+ value = "value",
+ isCopyable = true,
+ )
+
+ EMPTY_VIEW_STATES
+ .forEach { typeState ->
+ mutableStateFlow.update { currentState ->
+ currentState.copy(
+ viewState = typeState.copy(
+ type = DEFAULT_IDENTITY,
+ common = EMPTY_COMMON.copy(
+ notes = "this is a note",
+ customFields = listOf(textField),
+ ),
+ ),
+ )
+ }
+ }
+
+ composeTestRule.onNodeWithTextAfterScroll(textField.name)
+
+ composeTestRule
+ .onNodeWithTag("CipherNotesCopyButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
+ }
+ }
+
+ @Test
+ fun `on secure note copy notes field click should send CopyNotesClick`() {
+
+ mutableStateFlow.update { currentState ->
+ currentState.copy(
+ viewState = DEFAULT_SECURE_NOTE_VIEW_STATE,
+ )
+ }
+ composeTestRule.onNodeWithTextAfterScroll("Lots of notes")
+
+ composeTestRule
+ .onNodeWithTag("CipherNotesCopyButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
+ }
+ }
//endregion common
//region login
@@ -1927,6 +2036,144 @@ class VaultItemScreenTest : BaseComposeTest() {
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
}
+
+ @Test
+ fun `in identity state, on copy identity name field click should send CopyIdentityNameClick`() {
+
+ val identityName = "the identity name"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(identityName)
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopyNameButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick)
+ }
+ }
+
+ @Test
+ fun `in identity state, on copy username field click should send CopyUsernameClick`() {
+ val username = "the username"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(username)
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopyUsernameButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyUsernameClick)
+ }
+ }
+
+ @Test
+ fun `in identity state, on copy company field click should send CopyCompanyClick`() {
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+
+ // Scroll to ssn so we can see the Copy company button but not have it covered by the FAB
+ composeTestRule.onNodeWithTextAfterScroll("the SSN")
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopyCompanyButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyCompanyClick)
+ }
+ }
+
+ @Test
+ fun `in identity state, on copy SSN field click should send CopySsnClick`() {
+ val ssn = "the SSN"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(ssn)
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopySsnButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopySsnClick)
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `in identity state, on copy passport number field click should send CopyPassportNumberClick`() {
+ val passportNumber = "the passport number"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(passportNumber)
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopyPassportNumberButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick)
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `in identity state, on copy license number field click should send CopyLicenseNumberClick`() {
+ val licenseNumber = "the license number"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(licenseNumber)
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopyLicenseNumberButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyLicenseNumberClick)
+ }
+ }
+
+ @Test
+ fun `in identity state, on copy email field click should send CopyEmailClick`() {
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onFirstNodeWithTextAfterScroll("the address")
+
+ composeTestRule
+ .onNodeWithContentDescriptionAfterScroll("Copy email")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyEmailClick)
+ }
+ }
+
+ @Test
+ fun `in identity state, on copy phone field click should send CopyPhoneClick`() {
+ val phone = "the phone number"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(phone)
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopyPhoneButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick)
+ }
+ }
+
+ @Test
+ fun `in identity state, on copy address field click should send CopyAddressClick`() {
+ val address = "the address"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(address)
+
+ composeTestRule
+ .onNodeWithTag("IdentityCopyAddressButton")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyAddressClick)
+ }
+ }
//endregion identity
//region card
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 89485e7a6..4a82e0c32 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
@@ -1670,6 +1670,33 @@ class VaultItemViewModelTest : BaseViewModelTest() {
coVerify { mockFileManager.delete(file) }
}
+
+ @Test
+ fun `on CopyNotesFieldClick should call setText on ClipboardManager`() {
+ every {
+ mockCipherView.toViewState(
+ previousState = null,
+ isPremiumUser = true,
+ hasMasterPassword = true,
+ totpCodeItemData = null,
+ canDelete = true,
+ canAssignToCollections = true,
+ )
+ } returns DEFAULT_VIEW_STATE
+
+ mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
+ mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
+ mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
+
+ val notes = "Lots of notes"
+ every { clipboardManager.setText(text = notes) } just runs
+
+ viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = notes)
+ }
+ }
}
@Nested
@@ -2527,6 +2554,143 @@ class VaultItemViewModelTest : BaseViewModelTest() {
}
}
+ @Nested
+ inner class IdentityActions {
+ private lateinit var viewModel: VaultItemViewModel
+
+ @BeforeEach
+ fun setup() {
+ viewModel = createViewModel(
+ state = DEFAULT_STATE.copy(
+ viewState = IDENTITY_VIEW_STATE,
+ ),
+ )
+ every {
+ mockCipherView.toViewState(
+ previousState = null,
+ isPremiumUser = true,
+ hasMasterPassword = true,
+ totpCodeItemData = null,
+ canDelete = true,
+ canAssignToCollections = true,
+ )
+ } returns IDENTITY_VIEW_STATE
+ mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
+ mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
+ mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
+ }
+
+ @Test
+ fun `on CopyIdentityNameClick should copy fingerprint to clipboard`() =
+ runTest {
+ val username = "the username"
+ every { clipboardManager.setText(text = username) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyUsernameClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = username)
+ }
+ }
+
+ @Test
+ fun `on CopyUsernameClick should copy fingerprint to clipboard`() =
+ runTest {
+ val identityName = "the identity name"
+ every { clipboardManager.setText(text = identityName) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = identityName)
+ }
+ }
+
+ @Test
+ fun `on CopyCompanyClick should copy company to clipboard`() = runTest {
+ val company = "the company name"
+ every { clipboardManager.setText(text = company) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyCompanyClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = company)
+ }
+ }
+
+ @Test
+ fun `on CopySsnClick should copy SSN to clipboard`() = runTest {
+ val ssn = "the SSN"
+ every { clipboardManager.setText(text = ssn) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopySsnClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = ssn)
+ }
+ }
+
+ @Test
+ fun `on CopyPassportNumberClick should copy passport number to clipboard`() = runTest {
+ val passportNumber = "the passport number"
+ every { clipboardManager.setText(text = passportNumber) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = passportNumber)
+ }
+ }
+
+ @Test
+ fun `on CopyLicenseNumberClick should copy license number to clipboard`() = runTest {
+ val licenseNumber = "the license number"
+ every { clipboardManager.setText(text = licenseNumber) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyLicenseNumberClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = licenseNumber)
+ }
+ }
+
+ @Test
+ fun `on CopyEmailClick should copy email to clipboard`() = runTest {
+ val email = "the email address"
+ every { clipboardManager.setText(text = email) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyEmailClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = email)
+ }
+ }
+
+ @Test
+ fun `on CopyPhoneClick should copy phone to clipboard`() = runTest {
+ val phone = "the phone number"
+ every { clipboardManager.setText(text = phone) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = phone)
+ }
+ }
+
+ @Test
+ fun `on CopyAddressClick should copy address to clipboard`() = runTest {
+ val address = "the address"
+ every { clipboardManager.setText(text = address) } just runs
+
+ viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyAddressClick)
+
+ verify(exactly = 1) {
+ clipboardManager.setText(text = address)
+ }
+ }
+ }
+
@Nested
inner class VaultItemFlow {
@BeforeEach
@@ -2844,6 +3008,19 @@ class VaultItemViewModelTest : BaseViewModelTest() {
showPrivateKey = false,
)
+ private val DEFAULT_IDENTITY_TYPE: VaultItemState.ViewState.Content.ItemType.Identity =
+ VaultItemState.ViewState.Content.ItemType.Identity(
+ username = "the username",
+ identityName = "the identity name",
+ company = "the company name",
+ ssn = "the SSN",
+ passportNumber = "the passport number",
+ licenseNumber = "the license number",
+ email = "the email address",
+ phone = "the phone number",
+ address = "the address",
+ )
+
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
name = "login cipher",
@@ -2908,5 +3085,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
common = DEFAULT_COMMON,
type = DEFAULT_SSH_KEY_TYPE,
)
+
+ private val IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content =
+ VaultItemState.ViewState.Content(
+ common = DEFAULT_COMMON,
+ type = DEFAULT_IDENTITY_TYPE,
+ )
}
}