mirror of
https://github.com/bitwarden/android.git
synced 2026-03-15 15:31:34 -05:00
[PM-15864] Add copy private key action for SSH keys (#4462)
This commit is contained in:
@@ -15,7 +15,7 @@ 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.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
|
||||
@@ -84,12 +84,20 @@ fun VaultItemSshKeyContent(
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordField(
|
||||
BitwardenPasswordFieldWithActions(
|
||||
label = stringResource(id = R.string.private_key),
|
||||
value = sshKeyItemState.privateKey,
|
||||
onValueChange = { },
|
||||
singleLine = false,
|
||||
readOnly = true,
|
||||
actions = {
|
||||
BitwardenTonalIconButton(
|
||||
vectorIconRes = R.drawable.ic_copy,
|
||||
contentDescription = stringResource(id = R.string.copy_private_key),
|
||||
onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick,
|
||||
modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"),
|
||||
)
|
||||
},
|
||||
showPassword = sshKeyItemState.showPrivateKey,
|
||||
showPasswordTestTag = "ViewPrivateKeyButton",
|
||||
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,
|
||||
|
||||
@@ -790,6 +790,8 @@ class VaultItemViewModel @Inject constructor(
|
||||
handlePrivateKeyVisibilityClicked(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick -> handleCopyPrivateKeyClick()
|
||||
|
||||
VaultItemAction.ItemType.SshKey.CopyFingerprintClick -> handleCopyFingerprintClick()
|
||||
}
|
||||
}
|
||||
@@ -824,6 +826,20 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyPrivateKeyClick() {
|
||||
onSshKeyContent { content, sshKey ->
|
||||
if (content.common.requiresReprompt) {
|
||||
updateDialogState(
|
||||
VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = sshKey.privateKey),
|
||||
),
|
||||
)
|
||||
return@onSshKeyContent
|
||||
}
|
||||
clipboardManager.setText(text = sshKey.privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyFingerprintClick() {
|
||||
onSshKeyContent { _, sshKey ->
|
||||
clipboardManager.setText(text = sshKey.fingerprint)
|
||||
@@ -1960,6 +1976,11 @@ sealed class VaultItemAction {
|
||||
*/
|
||||
data class PrivateKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the private key.
|
||||
*/
|
||||
data object CopyPrivateKeyClick : SshKey()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the fingerprint.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
||||
data class VaultSshKeyItemTypeHandlers(
|
||||
val onCopyPublicKeyClick: () -> Unit,
|
||||
val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
|
||||
val onCopyPrivateKeyClick: () -> Unit,
|
||||
val onCopyFingerprintClick: () -> Unit,
|
||||
) {
|
||||
|
||||
@@ -34,6 +35,11 @@ data class VaultSshKeyItemTypeHandlers(
|
||||
),
|
||||
)
|
||||
},
|
||||
onCopyPrivateKeyClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick,
|
||||
)
|
||||
},
|
||||
onCopyFingerprintClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.SshKey.CopyFingerprintClick,
|
||||
|
||||
@@ -1124,4 +1124,5 @@ Do you want to switch to this account?</string>
|
||||
<string name="copied_to_clipboard">Copied to clipboard.</string>
|
||||
<string name="we_couldnt_verify_the_servers_certificate">We couldn’t verify the server’s certificate. The certificate chain or proxy settings on your device or your Bitwarden server may not be set up correctly.</string>
|
||||
<string name="review_flow_launched">Review flow launched!</string>
|
||||
<string name="copy_private_key">Copy private key</string>
|
||||
</resources>
|
||||
|
||||
@@ -2545,6 +2545,18 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ssh key state, on copy private key click should send CopyPrivateKeyClick`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll("Copy private key")
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ssh key state, fingerprint should be displayed according to state`() {
|
||||
val fingerprint = "the fingerprint"
|
||||
|
||||
@@ -2583,6 +2583,71 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `onPrivateKeyCopyClick should copy private key to clipboard when re-prompt is not required`() =
|
||||
runTest {
|
||||
every { clipboardManager.setText("mockPrivateKey") } just runs
|
||||
every {
|
||||
mockCipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
)
|
||||
} returns createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
type = DEFAULT_SSH_KEY_TYPE,
|
||||
)
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = DEFAULT_SSH_KEY_TYPE.privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onPrivateKeyCopyClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val sshKeyState = DEFAULT_STATE.copy(viewState = SSH_KEY_VIEW_STATE)
|
||||
every { clipboardManager.setText("mockPrivateKey") } just runs
|
||||
every {
|
||||
mockCipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
hasMasterPassword = true,
|
||||
totpCodeItemData = null,
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
)
|
||||
} returns SSH_KEY_VIEW_STATE
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick)
|
||||
|
||||
assertEquals(
|
||||
sshKeyState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(
|
||||
value = DEFAULT_SSH_KEY_TYPE.privateKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(text = DEFAULT_SSH_KEY_TYPE.privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyFingerprintClick should copy fingerprint to clipboard`() = runTest {
|
||||
every { clipboardManager.setText("mockFingerprint") } just runs
|
||||
|
||||
Reference in New Issue
Block a user