[PM-14273] Add copy functionality for SSH key fields (#4204)

This commit is contained in:
Patrick Honkonen 2024-10-31 16:41:13 -04:00 committed by GitHub
parent 0844939eca
commit 2f6578fd5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 133 additions and 4 deletions

View file

@ -12,8 +12,11 @@ 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.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
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.VaultSshKeyItemTypeHandlers
@ -55,16 +58,24 @@ fun VaultItemSshKeyContent(
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.public_key),
value = sshKeyItemState.publicKey,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenTonalIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_public_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick,
modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"),
)
},
modifier = Modifier
.testTag("SshKeyItemPublicKeyEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@ -88,16 +99,24 @@ fun VaultItemSshKeyContent(
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.fingerprint),
value = sshKeyItemState.fingerprint,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenTonalIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_fingerprint),
onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick,
modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"),
)
},
modifier = Modifier
.testTag("SshKeyItemFingerprintEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}

View file

@ -758,9 +758,19 @@ class VaultItemViewModel @Inject constructor(
private fun handleSshKeyTypeActions(action: VaultItemAction.ItemType.SshKey) {
when (action) {
VaultItemAction.ItemType.SshKey.CopyPublicKeyClick -> handleCopyPublicKeyClick()
is VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked -> {
handlePrivateKeyVisibilityClicked(action)
}
VaultItemAction.ItemType.SshKey.CopyFingerprintClick -> handleCopyFingerprintClick()
}
}
private fun handleCopyPublicKeyClick() {
onSshKeyContent { _, sshKey ->
clipboardManager.setText(text = sshKey.publicKey)
}
}
@ -778,6 +788,12 @@ class VaultItemViewModel @Inject constructor(
}
}
private fun handleCopyFingerprintClick() {
onSshKeyContent { _, sshKey ->
clipboardManager.setText(text = sshKey.fingerprint)
}
}
//endregion SSH Key Type Handlers
//region Internal Type Handlers
@ -1758,10 +1774,20 @@ sealed class VaultItemAction {
* Represents actions specific to the SshKey type.
*/
sealed class SshKey : ItemType() {
/**
* The user has clicked the copy button for the public key.
*/
data object CopyPublicKeyClick : SshKey()
/**
* The user has clicked to display the private key.
*/
data class PrivateKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
/**
* The user has clicked the copy button for the fingerprint.
*/
data object CopyFingerprintClick : SshKey()
}
}

View file

@ -8,7 +8,9 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
* items in a vault.
*/
data class VaultSshKeyItemTypeHandlers(
val onCopyPublicKeyClick: () -> Unit,
val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
val onCopyFingerprintClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
@ -20,6 +22,11 @@ data class VaultSshKeyItemTypeHandlers(
@Suppress("LongMethod")
fun create(viewModel: VaultItemViewModel): VaultSshKeyItemTypeHandlers =
VaultSshKeyItemTypeHandlers(
onCopyPublicKeyClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.CopyPublicKeyClick,
)
},
onShowPrivateKeyClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
@ -27,6 +34,11 @@ data class VaultSshKeyItemTypeHandlers(
),
)
},
onCopyFingerprintClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.CopyFingerprintClick,
)
},
)
}
}

View file

@ -1079,4 +1079,6 @@ Do you want to switch to this account?</string>
<string name="public_key">Public key</string>
<string name="private_key">Private key</string>
<string name="ssh_keys">SSH keys</string>
<string name="copy_public_key">Copy public key</string>
<string name="copy_fingerprint">Copy fingerprint</string>
</resources>

View file

@ -2153,6 +2153,18 @@ class VaultItemScreenTest : BaseComposeTest() {
composeTestRule.onNodeWithTextAfterScroll(publicKey).assertIsDisplayed()
}
@Test
fun `in ssh key state, on copy public key click should send CopyPublicKeyClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy public key")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPublicKeyClick)
}
}
@Test
fun `in ssh key state, private key should be displayed according to state`() {
val privateKey = "the private key"
@ -2195,6 +2207,18 @@ class VaultItemScreenTest : BaseComposeTest() {
composeTestRule.onNodeWithTextAfterScroll(fingerprint).assertIsDisplayed()
}
@Test
fun `in ssh key state, on copy fingerprint click should send CopyFingerprintClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescription("Copy fingerprint")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyFingerprintClick)
}
}
//endregion ssh key
}

View file

@ -2349,6 +2349,29 @@ class VaultItemViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `on CopyPublicKeyClick should copy public key to clipboard`() = runTest {
every { clipboardManager.setText("mockPublicKey") } just runs
val mockCipherView = mockk<CipherView> {
every {
toViewState(
previousState = null,
isPremiumUser = true,
hasMasterPassword = true,
totpCodeItemData = null,
)
} returns SSH_KEY_VIEW_STATE
}
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPublicKeyClick)
verify(exactly = 1) {
clipboardManager.setText(text = DEFAULT_SSH_KEY_TYPE.publicKey)
}
}
@Suppress("MaxLineLength")
@Test
fun `on PrivateKeyVisibilityClick should show password dialog when re-prompt is required`() =
@ -2388,6 +2411,29 @@ class VaultItemViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
}
@Test
fun `on CopyFingerprintClick should copy fingerprint to clipboard`() = runTest {
every { clipboardManager.setText("mockFingerprint") } just runs
val mockCipherView = mockk<CipherView> {
every {
toViewState(
previousState = null,
isPremiumUser = true,
hasMasterPassword = true,
totpCodeItemData = null,
)
} returns SSH_KEY_VIEW_STATE
}
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyFingerprintClick)
verify(exactly = 1) {
clipboardManager.setText(text = DEFAULT_SSH_KEY_TYPE.fingerprint)
}
}
}
@Nested