mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 09:56:11 +03:00
[PM-14273] Add copy functionality for SSH key fields (#4204)
This commit is contained in:
parent
0844939eca
commit
2f6578fd5a
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