mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
[PM-13831] Add copy button identity and note fields (#4302)
This commit is contained in:
parent
531b003347
commit
d418444dc0
11 changed files with 798 additions and 58 deletions
|
@ -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.button.BitwardenTonalIconButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
|
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.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.components.header.BitwardenListHeaderText
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
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.VaultCommonItemTypeHandlers
|
||||||
|
@ -173,14 +174,22 @@ fun VaultItemCardContent(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
BitwardenTextFieldWithActions(
|
||||||
label = stringResource(id = R.string.notes),
|
label = stringResource(id = R.string.notes),
|
||||||
value = notes,
|
value = notes,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
singleLine = false,
|
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
|
modifier = Modifier
|
||||||
.testTag("CipherNotesLabel")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,19 +13,23 @@ import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.x8bit.bitwarden.R
|
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.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.components.header.BitwardenListHeaderText
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
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.
|
* The top level content UI state for the [VaultItemScreen] when viewing a Identity cipher.
|
||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod", "MaxLineLength")
|
||||||
@Composable
|
@Composable
|
||||||
fun VaultItemIdentityContent(
|
fun VaultItemIdentityContent(
|
||||||
identityState: VaultItemState.ViewState.Content.ItemType.Identity,
|
identityState: VaultItemState.ViewState.Content.ItemType.Identity,
|
||||||
commonState: VaultItemState.ViewState.Content.Common,
|
commonState: VaultItemState.ViewState.Content.Common,
|
||||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||||
|
vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier = modifier) {
|
LazyColumn(modifier = modifier) {
|
||||||
|
@ -54,14 +58,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.identityName?.let { identityName ->
|
identityState.identityName?.let { identityName ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.identity_name),
|
label = stringResource(id = R.string.identity_name),
|
||||||
value = identityName,
|
value = identityName,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_identity_name),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityNameEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyNameButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityNameEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -70,14 +74,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.username?.let { username ->
|
identityState.username?.let { username ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.username),
|
label = stringResource(id = R.string.username),
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_username),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityUsernameEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyUsernameButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityUsernameEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -86,14 +90,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.company?.let { company ->
|
identityState.company?.let { company ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.company),
|
label = stringResource(id = R.string.company),
|
||||||
value = company,
|
value = company,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_company),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityCompanyEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyCompanyButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityCompanyEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -102,14 +106,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.ssn?.let { ssn ->
|
identityState.ssn?.let { ssn ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.ssn),
|
label = stringResource(id = R.string.ssn),
|
||||||
value = ssn,
|
value = ssn,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_ssn),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentitySsnEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopySsnButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentitySsnEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -118,14 +122,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.passportNumber?.let { passportNumber ->
|
identityState.passportNumber?.let { passportNumber ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.passport_number),
|
label = stringResource(id = R.string.passport_number),
|
||||||
value = passportNumber,
|
value = passportNumber,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_passport_number),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityPassportNumberEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyPassportNumberButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityPassportNumberEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -134,14 +138,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.licenseNumber?.let { licenseNumber ->
|
identityState.licenseNumber?.let { licenseNumber ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.license_number),
|
label = stringResource(id = R.string.license_number),
|
||||||
value = licenseNumber,
|
value = licenseNumber,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_license_number),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityLicenseNumberEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyLicenseNumberButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityLicenseNumberEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -150,14 +154,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.email?.let { email ->
|
identityState.email?.let { email ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.email),
|
label = stringResource(id = R.string.email),
|
||||||
value = email,
|
value = email,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_email),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityEmailEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyEmailButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityEmailEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -166,14 +170,14 @@ fun VaultItemIdentityContent(
|
||||||
identityState.phone?.let { phone ->
|
identityState.phone?.let { phone ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.phone),
|
label = stringResource(id = R.string.phone),
|
||||||
value = phone,
|
value = phone,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_phone),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityPhoneEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyPhoneButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityPhoneEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -182,20 +186,19 @@ fun VaultItemIdentityContent(
|
||||||
identityState.address?.let { address ->
|
identityState.address?.let { address ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.address),
|
label = stringResource(id = R.string.address),
|
||||||
value = address,
|
value = address,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_address),
|
||||||
readOnly = true,
|
textFieldTestTag = "IdentityAddressEntry",
|
||||||
singleLine = false,
|
copyActionTestTag = "IdentityCopyAddressButton",
|
||||||
|
onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("IdentityAddressEntry")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commonState.notes?.let { notes ->
|
commonState.notes?.let { notes ->
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
@ -206,14 +209,14 @@ fun VaultItemIdentityContent(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
IdentityCopyField(
|
||||||
label = stringResource(id = R.string.notes),
|
label = stringResource(id = R.string.notes),
|
||||||
value = notes,
|
value = notes,
|
||||||
onValueChange = { },
|
copyContentDescription = stringResource(id = R.string.copy_notes),
|
||||||
readOnly = true,
|
textFieldTestTag = "CipherNotesLabel",
|
||||||
singleLine = false,
|
copyActionTestTag = "CipherNotesCopyButton",
|
||||||
|
onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("CipherNotesLabel")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -158,8 +158,8 @@ fun VaultItemLoginContent(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
NotesField(
|
NotesField(
|
||||||
notes = notes,
|
notes = notes,
|
||||||
|
onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("CipherNotesLabel")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
@ -273,14 +273,24 @@ private fun Fido2CredentialField(
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotesField(
|
private fun NotesField(
|
||||||
notes: String,
|
notes: String,
|
||||||
|
onCopyAction: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
BitwardenTextField(
|
BitwardenTextFieldWithActions(
|
||||||
label = stringResource(id = R.string.notes),
|
label = stringResource(id = R.string.notes),
|
||||||
value = notes,
|
value = notes,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
singleLine = false,
|
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,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.platform.util.persistentListOfNotNull
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
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.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.VaultLoginItemTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
||||||
|
|
||||||
|
@ -272,6 +273,9 @@ fun VaultItemScreen(
|
||||||
vaultSshKeyItemTypeHandlers = remember(viewModel) {
|
vaultSshKeyItemTypeHandlers = remember(viewModel) {
|
||||||
VaultSshKeyItemTypeHandlers.create(viewModel = viewModel)
|
VaultSshKeyItemTypeHandlers.create(viewModel = viewModel)
|
||||||
},
|
},
|
||||||
|
vaultIdentityItemTypeHandlers = remember(viewModel) {
|
||||||
|
VaultIdentityItemTypeHandlers.create(viewModel = viewModel)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -350,6 +354,7 @@ private fun VaultItemContent(
|
||||||
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
||||||
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
|
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
|
||||||
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
|
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
|
||||||
|
vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (viewState) {
|
when (viewState) {
|
||||||
|
@ -386,6 +391,7 @@ private fun VaultItemContent(
|
||||||
commonState = viewState.common,
|
commonState = viewState.common,
|
||||||
identityState = viewState.type,
|
identityState = viewState.type,
|
||||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||||
|
vaultIdentityItemTypeHandlers = vaultIdentityItemTypeHandlers,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,9 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.x8bit.bitwarden.R
|
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.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.components.header.BitwardenListHeaderText
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||||
|
@ -66,14 +68,22 @@ fun VaultItemSecureNoteContent(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
BitwardenTextFieldWithActions(
|
||||||
label = stringResource(id = R.string.notes),
|
label = stringResource(id = R.string.notes),
|
||||||
value = notes,
|
value = notes,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
singleLine = false,
|
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
|
modifier = Modifier
|
||||||
.testTag("CipherNotesLabel")
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
|
|
@ -133,6 +133,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
|
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
|
||||||
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
|
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
|
||||||
is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action)
|
is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action)
|
||||||
|
is VaultItemAction.ItemType.Identity -> handleIdentityTypeActions(action)
|
||||||
is VaultItemAction.Common -> handleCommonActions(action)
|
is VaultItemAction.Common -> handleCommonActions(action)
|
||||||
is VaultItemAction.Internal -> handleInternalAction(action)
|
is VaultItemAction.Internal -> handleInternalAction(action)
|
||||||
}
|
}
|
||||||
|
@ -184,6 +185,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is VaultItemAction.Common.RestoreVaultItemClick -> handleRestoreItemClicked()
|
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
|
//endregion Common Handlers
|
||||||
|
|
||||||
//region Login Type Handlers
|
//region Login Type Handlers
|
||||||
|
@ -812,6 +821,99 @@ class VaultItemViewModel @Inject constructor(
|
||||||
|
|
||||||
//endregion SSH Key Type Handlers
|
//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
|
//region Internal Type Handlers
|
||||||
|
|
||||||
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
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.
|
* The user confirmed cloning a cipher without its FIDO 2 credentials.
|
||||||
*/
|
*/
|
||||||
data object ConfirmCloneWithoutFido2CredentialClick : Common()
|
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()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,7 @@ data class VaultCommonItemTypeHandlers(
|
||||||
Boolean,
|
Boolean,
|
||||||
) -> Unit,
|
) -> Unit,
|
||||||
val onAttachmentDownloadClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit,
|
val onAttachmentDownloadClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit,
|
||||||
|
val onCopyNotesClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@Suppress("UndocumentedPublicClass")
|
@Suppress("UndocumentedPublicClass")
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -52,6 +53,9 @@ data class VaultCommonItemTypeHandlers(
|
||||||
onAttachmentDownloadClick = {
|
onAttachmentDownloadClick = {
|
||||||
viewModel.trySendAction(VaultItemAction.Common.AttachmentDownloadClick(it))
|
viewModel.trySendAction(VaultItemAction.Common.AttachmentDownloadClick(it))
|
||||||
},
|
},
|
||||||
|
onCopyNotesClick = {
|
||||||
|
viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1087,4 +1087,12 @@ Do you want to switch to this account?</string>
|
||||||
<string name="skip_for_now">Skip for now</string>
|
<string name="skip_for_now">Skip for now</string>
|
||||||
<string name="done_text">Done</string>
|
<string name="done_text">Done</string>
|
||||||
<string name="page_number_x_of_y">%1$s of %2$s</string>
|
<string name="page_number_x_of_y">%1$s of %2$s</string>
|
||||||
|
<string name="copy_identity_name">Copy identity name</string>
|
||||||
|
<string name="copy_company">Copy company</string>
|
||||||
|
<string name="copy_ssn">Copy social security number</string>
|
||||||
|
<string name="copy_passport_number">Copy passport number</string>
|
||||||
|
<string name="copy_license_number">Copy license number</string>
|
||||||
|
<string name="copy_email">Copy email</string>
|
||||||
|
<string name="copy_phone">Copy phone number</string>
|
||||||
|
<string name="copy_address">Copy address</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onChildren
|
import androidx.compose.ui.test.onChildren
|
||||||
import androidx.compose.ui.test.onLast
|
import androidx.compose.ui.test.onLast
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.onSiblings
|
import androidx.compose.ui.test.onSiblings
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
@ -1257,6 +1258,114 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
.filterToOne(hasAnyAncestor(isPopup()))
|
.filterToOne(hasAnyAncestor(isPopup()))
|
||||||
.assertDoesNotExist()
|
.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
|
//endregion common
|
||||||
|
|
||||||
//region login
|
//region login
|
||||||
|
@ -1927,6 +2036,144 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
composeTestRule.assertScrollableNodeDoesNotExist(identityName)
|
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
|
//endregion identity
|
||||||
|
|
||||||
//region card
|
//region card
|
||||||
|
|
|
@ -1670,6 +1670,33 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
coVerify { mockFileManager.delete(file) }
|
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
|
@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
|
@Nested
|
||||||
inner class VaultItemFlow {
|
inner class VaultItemFlow {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -2844,6 +3008,19 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
showPrivateKey = false,
|
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 =
|
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||||
VaultItemState.ViewState.Content.Common(
|
VaultItemState.ViewState.Content.Common(
|
||||||
name = "login cipher",
|
name = "login cipher",
|
||||||
|
@ -2908,5 +3085,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
common = DEFAULT_COMMON,
|
common = DEFAULT_COMMON,
|
||||||
type = DEFAULT_SSH_KEY_TYPE,
|
type = DEFAULT_SSH_KEY_TYPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||||
|
VaultItemState.ViewState.Content(
|
||||||
|
common = DEFAULT_COMMON,
|
||||||
|
type = DEFAULT_IDENTITY_TYPE,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue