mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-14273 Add copy functionality for SSH key public key and fingerprint
This commit adds the ability to copy the public key and fingerprint of an SSH key within the vault item view. It introduces new buttons to trigger the copy actions and updates the view model and item handlers to handle the copy functionality.
This commit is contained in:
parent
eaa7923d1f
commit
5ab42296ce
6 changed files with 133 additions and 4 deletions
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue