mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 12:30:00 +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.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.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.BitwardenPasswordField
|
||||||
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.VaultSshKeyItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
||||||
|
|
||||||
|
@ -55,16 +58,24 @@ fun VaultItemSshKeyContent(
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
BitwardenTextFieldWithActions(
|
||||||
label = stringResource(id = R.string.public_key),
|
label = stringResource(id = R.string.public_key),
|
||||||
value = sshKeyItemState.publicKey,
|
value = sshKeyItemState.publicKey,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
readOnly = true,
|
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
|
modifier = Modifier
|
||||||
.testTag("SshKeyItemPublicKeyEntry")
|
.testTag("SshKeyItemPublicKeyEntry")
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,16 +99,24 @@ fun VaultItemSshKeyContent(
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextField(
|
BitwardenTextFieldWithActions(
|
||||||
label = stringResource(id = R.string.fingerprint),
|
label = stringResource(id = R.string.fingerprint),
|
||||||
value = sshKeyItemState.fingerprint,
|
value = sshKeyItemState.fingerprint,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
readOnly = true,
|
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
|
modifier = Modifier
|
||||||
.testTag("SshKeyItemFingerprintEntry")
|
.testTag("SshKeyItemFingerprintEntry")
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -758,9 +758,19 @@ class VaultItemViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun handleSshKeyTypeActions(action: VaultItemAction.ItemType.SshKey) {
|
private fun handleSshKeyTypeActions(action: VaultItemAction.ItemType.SshKey) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
VaultItemAction.ItemType.SshKey.CopyPublicKeyClick -> handleCopyPublicKeyClick()
|
||||||
|
|
||||||
is VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked -> {
|
is VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked -> {
|
||||||
handlePrivateKeyVisibilityClicked(action)
|
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
|
//endregion SSH Key Type Handlers
|
||||||
|
|
||||||
//region Internal Type Handlers
|
//region Internal Type Handlers
|
||||||
|
@ -1758,10 +1774,20 @@ sealed class VaultItemAction {
|
||||||
* Represents actions specific to the SshKey type.
|
* Represents actions specific to the SshKey type.
|
||||||
*/
|
*/
|
||||||
sealed class SshKey : ItemType() {
|
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.
|
* The user has clicked to display the private key.
|
||||||
*/
|
*/
|
||||||
data class PrivateKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
|
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.
|
* items in a vault.
|
||||||
*/
|
*/
|
||||||
data class VaultSshKeyItemTypeHandlers(
|
data class VaultSshKeyItemTypeHandlers(
|
||||||
|
val onCopyPublicKeyClick: () -> Unit,
|
||||||
val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
|
val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
|
||||||
|
val onCopyFingerprintClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Suppress("UndocumentedPublicClass")
|
@Suppress("UndocumentedPublicClass")
|
||||||
|
@ -20,6 +22,11 @@ data class VaultSshKeyItemTypeHandlers(
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
fun create(viewModel: VaultItemViewModel): VaultSshKeyItemTypeHandlers =
|
fun create(viewModel: VaultItemViewModel): VaultSshKeyItemTypeHandlers =
|
||||||
VaultSshKeyItemTypeHandlers(
|
VaultSshKeyItemTypeHandlers(
|
||||||
|
onCopyPublicKeyClick = {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.SshKey.CopyPublicKeyClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
onShowPrivateKeyClick = {
|
onShowPrivateKeyClick = {
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
|
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="public_key">Public key</string>
|
||||||
<string name="private_key">Private key</string>
|
<string name="private_key">Private key</string>
|
||||||
<string name="ssh_keys">SSH keys</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>
|
</resources>
|
||||||
|
|
|
@ -2153,6 +2153,18 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.onNodeWithTextAfterScroll(publicKey).assertIsDisplayed()
|
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
|
@Test
|
||||||
fun `in ssh key state, private key should be displayed according to state`() {
|
fun `in ssh key state, private key should be displayed according to state`() {
|
||||||
val privateKey = "the private key"
|
val privateKey = "the private key"
|
||||||
|
@ -2195,6 +2207,18 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.onNodeWithTextAfterScroll(fingerprint).assertIsDisplayed()
|
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
|
//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")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on PrivateKeyVisibilityClick should show password dialog when re-prompt is required`() =
|
fun `on PrivateKeyVisibilityClick should show password dialog when re-prompt is required`() =
|
||||||
|
@ -2388,6 +2411,29 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
viewModel.stateFlow.value,
|
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
|
@Nested
|
||||||
|
|
Loading…
Add table
Reference in a new issue