mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 02:38:50 +03:00
Merge branch 'bitwarden:main' into main
This commit is contained in:
commit
e4fa16ac3e
24 changed files with 180 additions and 187 deletions
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -190,7 +191,7 @@ class GeneratorRepositoryImpl(
|
|||
|
||||
override suspend fun generateForwardedServiceUsername(
|
||||
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
|
||||
): GeneratedForwardedServiceUsernameResult =
|
||||
): GeneratedForwardedServiceUsernameResult = withContext(scope.coroutineContext) {
|
||||
generatorSdkSource.generateForwardedServiceEmail(forwardedServiceGeneratorRequest)
|
||||
.fold(
|
||||
onSuccess = { generatedEmail ->
|
||||
|
@ -200,6 +201,7 @@ class GeneratorRepositoryImpl(
|
|||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPasscodeGenerationOptions(): PasscodeGenerationOptions? {
|
||||
val userId = authDiskSource.userState?.activeUserId
|
||||
|
|
|
@ -21,7 +21,7 @@ import kotlin.math.floor
|
|||
* This character takes up no space but can be used to ensure a string is not empty. It can also
|
||||
* be used to insert "safe" line-break positions in a string.
|
||||
*
|
||||
* Note: Is a string only contains this charactor, it is _not_ considered blank.
|
||||
* Note: Is a string only contains this character, it is _not_ considered blank.
|
||||
*/
|
||||
const val ZERO_WIDTH_CHARACTER: String = "\u200B"
|
||||
|
||||
|
@ -126,14 +126,15 @@ fun String.withLineBreaksAtWidth(
|
|||
): String {
|
||||
val measurer = rememberTextMeasurer()
|
||||
return remember(this, widthPx, monospacedTextStyle) {
|
||||
val characterSizePx = measurer
|
||||
.measure("*", monospacedTextStyle)
|
||||
.size
|
||||
.width
|
||||
val perLineCharacterLimit = floor(widthPx / characterSizePx).toInt()
|
||||
if (widthPx > 0) {
|
||||
if (widthPx > 0 && this.isNotEmpty()) {
|
||||
val stringLengthPx = measurer
|
||||
.measure(text = this, softWrap = false, style = monospacedTextStyle)
|
||||
.size
|
||||
.width
|
||||
val linesRequired = stringLengthPx / widthPx
|
||||
val charsPerLine = floor(this.length / linesRequired).toInt()
|
||||
this
|
||||
.chunked(perLineCharacterLimit)
|
||||
.chunked(size = charsPerLine)
|
||||
.joinToString(separator = "\n")
|
||||
} else {
|
||||
this
|
||||
|
|
|
@ -84,6 +84,7 @@ fun BitwardenSearchTopAppBar(
|
|||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
modifier = Modifier
|
||||
.tabNavigation()
|
||||
.testTag("SearchFieldEntry")
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
|
|
@ -17,7 +17,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
|
@ -65,6 +64,7 @@ fun BitwardenOverflowActionItem(
|
|||
containerColor = BitwardenTheme.colorScheme.background.primary,
|
||||
modifier = Modifier
|
||||
.semantics { testTagsAsResourceId = true }
|
||||
.testTag("FloatingOptionsContent")
|
||||
.widthIn(
|
||||
min = 112.dp,
|
||||
max = 280.dp,
|
||||
|
@ -72,16 +72,13 @@ fun BitwardenOverflowActionItem(
|
|||
content = {
|
||||
menuItemDataList.forEach { dropdownMenuItemData ->
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.semantics {
|
||||
dropdownMenuItemData.testTag?.let {
|
||||
testTag = it
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag("FloatingOptionsItem"),
|
||||
colors = bitwardenMenuItemColors(),
|
||||
text = {
|
||||
Text(
|
||||
text = dropdownMenuItemData.text,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
modifier = Modifier.testTag("FloatingOptionsItemName"),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
|
@ -115,10 +112,8 @@ private fun BitwardenOverflowActionItem_preview() {
|
|||
*
|
||||
* @param text The text displayed for the item in the menu.
|
||||
* @param onClick A callback for when the menu item is clicked.
|
||||
* @param testTag Optional test tag for the menu item.
|
||||
*/
|
||||
data class OverflowMenuItemData(
|
||||
val text: String,
|
||||
val onClick: () -> Unit,
|
||||
val testTag: String? = null,
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.selected
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -35,6 +36,7 @@ fun BitwardenSelectionRow(
|
|||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.testTag("AlertRadioButtonOption")
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(
|
||||
|
@ -56,6 +58,7 @@ fun BitwardenSelectionRow(
|
|||
text = text(),
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
modifier = Modifier.testTag("AlertRadioButtonOptionName"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
|
@ -167,7 +168,7 @@ fun BitwardenListItem(
|
|||
selectionItems = {
|
||||
selectionDataList.forEach { itemData ->
|
||||
BitwardenBasicDialogRow(
|
||||
modifier = Modifier.semantics { itemData.testTag?.let { testTag = it } },
|
||||
modifier = Modifier.testTag("AlertSelectionOption"),
|
||||
text = itemData.text,
|
||||
onClick = {
|
||||
shouldShowDialog = false
|
||||
|
|
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
|
@ -90,7 +89,6 @@ fun SearchScreen(
|
|||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = searchHandlers.onBackClick,
|
||||
),
|
||||
modifier = Modifier.testTag(tag = "SearchFieldEntry"),
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
|
|
|
@ -460,7 +460,7 @@ private fun ColumnScope.PasswordTypeContent(
|
|||
valueTag = "PasswordLengthLabel",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 16.dp),
|
||||
.padding(end = 28.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
|
|
@ -351,7 +351,7 @@ class GeneratorViewModel @Inject constructor(
|
|||
GeneratorState.MainType.Passphrase(
|
||||
numWords = max(options.numWords, minNumWords),
|
||||
minNumWords = minNumWords,
|
||||
wordSeparator = options.wordSeparator.toCharArray().first(),
|
||||
wordSeparator = options.wordSeparator.toCharArray().firstOrNull(),
|
||||
capitalize = options.allowCapitalize || policy.capitalize == true,
|
||||
capitalizeEnabled = policy.capitalize != true,
|
||||
includeNumber = options.allowIncludeNumber || policy.includeNumber == true,
|
||||
|
@ -462,7 +462,7 @@ class GeneratorViewModel @Inject constructor(
|
|||
val newOptions = options.copy(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSPHRASE,
|
||||
numWords = passphrase.numWords,
|
||||
wordSeparator = passphrase.wordSeparator.toString(),
|
||||
wordSeparator = passphrase.wordSeparator?.toString().orEmpty(),
|
||||
allowCapitalize = passphrase.capitalize,
|
||||
allowIncludeNumber = passphrase.includeNumber,
|
||||
)
|
||||
|
|
|
@ -82,7 +82,6 @@ fun PasswordHistoryScreen(
|
|||
BitwardenOverflowActionItem(
|
||||
menuItemDataList = persistentListOf(
|
||||
OverflowMenuItemData(
|
||||
testTag = "ClearPasswordList",
|
||||
text = stringResource(id = R.string.clear),
|
||||
onClick = remember(viewModel) {
|
||||
{
|
||||
|
|
|
@ -78,7 +78,6 @@ fun SendListItem(
|
|||
SelectionItemData(
|
||||
text = stringResource(id = R.string.copy_link),
|
||||
onClick = onCopyClick,
|
||||
testTag = "Copy",
|
||||
),
|
||||
SelectionItemData(
|
||||
text = stringResource(id = R.string.share_link),
|
||||
|
|
|
@ -46,7 +46,8 @@ fun LazyListScope.vaultAddEditSshKeyItems(
|
|||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.public_key),
|
||||
value = sshKeyState.publicKey,
|
||||
onValueChange = sshKeyTypeHandlers.onPublicKeyTextChange,
|
||||
readOnly = true,
|
||||
onValueChange = { },
|
||||
modifier = Modifier
|
||||
.testTag("PublicKeyEntry")
|
||||
.fillMaxWidth()
|
||||
|
@ -59,7 +60,8 @@ fun LazyListScope.vaultAddEditSshKeyItems(
|
|||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.private_key),
|
||||
value = sshKeyState.privateKey,
|
||||
onValueChange = sshKeyTypeHandlers.onPrivateKeyTextChange,
|
||||
readOnly = true,
|
||||
onValueChange = { /* no-op */ },
|
||||
showPassword = sshKeyState.showPrivateKey,
|
||||
showPasswordChange = { sshKeyTypeHandlers.onPrivateKeyVisibilityChange(it) },
|
||||
showPasswordTestTag = "ViewPrivateKeyButton",
|
||||
|
@ -75,7 +77,8 @@ fun LazyListScope.vaultAddEditSshKeyItems(
|
|||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.fingerprint),
|
||||
value = sshKeyState.fingerprint,
|
||||
onValueChange = sshKeyTypeHandlers.onFingerprintTextChange,
|
||||
readOnly = true,
|
||||
onValueChange = { /* no-op */ },
|
||||
modifier = Modifier
|
||||
.testTag("FingerprintEntry")
|
||||
.fillMaxWidth()
|
||||
|
@ -116,10 +119,7 @@ private fun VaultAddEditSshKeyItems_preview() {
|
|||
onHiddenFieldVisibilityChange = { },
|
||||
),
|
||||
sshKeyTypeHandlers = VaultAddEditSshKeyTypeHandlers(
|
||||
onPublicKeyTextChange = { },
|
||||
onPrivateKeyTextChange = { },
|
||||
onPrivateKeyVisibilityChange = { },
|
||||
onFingerprintTextChange = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1394,48 +1394,18 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
|
||||
private fun handleSshKeyTypeActions(action: VaultAddEditAction.ItemType.SshKeyType) {
|
||||
when (action) {
|
||||
is VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange -> {
|
||||
handlePublicKeyTextChange(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange -> {
|
||||
handlePrivateKeyTextChange(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange -> {
|
||||
handlePrivateKeyVisibilityChange(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange -> {
|
||||
handleSshKeyFingerprintTextChange(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePublicKeyTextChange(
|
||||
action: VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange,
|
||||
) {
|
||||
updateSshKeyContent { it.copy(publicKey = action.publicKey) }
|
||||
}
|
||||
|
||||
private fun handlePrivateKeyTextChange(
|
||||
action: VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange,
|
||||
) {
|
||||
updateSshKeyContent { it.copy(privateKey = action.privateKey) }
|
||||
}
|
||||
|
||||
private fun handlePrivateKeyVisibilityChange(
|
||||
action: VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange,
|
||||
) {
|
||||
updateSshKeyContent { it.copy(showPrivateKey = action.isVisible) }
|
||||
}
|
||||
|
||||
private fun handleSshKeyFingerprintTextChange(
|
||||
action: VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange,
|
||||
) {
|
||||
updateSshKeyContent { it.copy(fingerprint = action.fingerprint) }
|
||||
}
|
||||
|
||||
//endregion SSH Key Type Handlers
|
||||
|
||||
//region Internal Type Handlers
|
||||
|
@ -3064,25 +3034,11 @@ sealed class VaultAddEditAction {
|
|||
* Represents actions specific to the SSH Key type.
|
||||
*/
|
||||
sealed class SshKeyType : ItemType() {
|
||||
/**
|
||||
* Fired when the public key text input is changed.
|
||||
*/
|
||||
data class PublicKeyTextChange(val publicKey: String) : SshKeyType()
|
||||
|
||||
/**
|
||||
* Fired when the private key text input is changed.
|
||||
*/
|
||||
data class PrivateKeyTextChange(val privateKey: String) : SshKeyType()
|
||||
|
||||
/**
|
||||
* Fired when the private key's visibility has changed.
|
||||
*/
|
||||
data class PrivateKeyVisibilityChange(val isVisible: Boolean) : SshKeyType()
|
||||
|
||||
/**
|
||||
* Fired when the fingerprint text input is changed.
|
||||
*/
|
||||
data class FingerprintTextChange(val fingerprint: String) : SshKeyType()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,16 +10,10 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
|
|||
* These handlers are used to update the ViewModel with user actions such as text changes and
|
||||
* visibility changes for different SSH key fields (public key, private key, fingerprint).
|
||||
*
|
||||
* @property onPublicKeyTextChange Handler for changes in the public key text field.
|
||||
* @property onPrivateKeyTextChange Handler for changes in the private key text field.
|
||||
* @property onPrivateKeyVisibilityChange Handler for toggling the visibility of the private key.
|
||||
* @property onFingerprintTextChange Handler for changes in the fingerprint text field.
|
||||
*/
|
||||
data class VaultAddEditSshKeyTypeHandlers(
|
||||
val onPublicKeyTextChange: (String) -> Unit,
|
||||
val onPrivateKeyTextChange: (String) -> Unit,
|
||||
val onPrivateKeyVisibilityChange: (Boolean) -> Unit,
|
||||
val onFingerprintTextChange: (String) -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
|
@ -31,20 +25,7 @@ data class VaultAddEditSshKeyTypeHandlers(
|
|||
*/
|
||||
fun create(viewModel: VaultAddEditViewModel): VaultAddEditSshKeyTypeHandlers =
|
||||
VaultAddEditSshKeyTypeHandlers(
|
||||
onPublicKeyTextChange = { newPublicKey ->
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
|
||||
publicKey = newPublicKey,
|
||||
),
|
||||
)
|
||||
},
|
||||
onPrivateKeyTextChange = { newPrivateKey ->
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
|
||||
privateKey = newPrivateKey,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
||||
onPrivateKeyVisibilityChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange(
|
||||
|
@ -52,13 +33,6 @@ data class VaultAddEditSshKeyTypeHandlers(
|
|||
),
|
||||
)
|
||||
},
|
||||
onFingerprintTextChange = { newFingerprint ->
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
|
||||
fingerprint = newFingerprint,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ fun VaultContent(
|
|||
showDivider = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag("FolderFilter")
|
||||
.testTag("VerificationCodesFilter")
|
||||
.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.test.hasContentDescription
|
|||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isEditable
|
||||
import androidx.compose.ui.test.isPopup
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
|
@ -3348,54 +3349,33 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_SshKeys changing the public key should trigger PublicKeyTextChange`() {
|
||||
fun `in ItemType_SshKeys the public key field should be read only`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Public key")
|
||||
.performTextInput("TestPublicKey")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
|
||||
publicKey = "TestPublicKey",
|
||||
),
|
||||
)
|
||||
}
|
||||
.onNodeWithTextAfterScroll(text = "Public key")
|
||||
.assertExists()
|
||||
.assert(!isEditable())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_SshKeys changing the private key should trigger PrivateKeyTextChange`() {
|
||||
fun `in ItemType_SshKeys the private key field should be read only`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Private key")
|
||||
.performTextInput("TestPrivateKey")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
|
||||
privateKey = "TestPrivateKey",
|
||||
),
|
||||
)
|
||||
}
|
||||
.onNodeWithTextAfterScroll(text = "Private key")
|
||||
.assertExists()
|
||||
.assert(!isEditable())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_SshKeys changing the fingerprint should trigger FingerprintTextChange`() {
|
||||
fun `in ItemType_SshKeys the fingerprint field should be read only`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll("Fingerprint")
|
||||
.performTextInput("TestFingerprint")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
|
||||
fingerprint = "TestFingerprint",
|
||||
),
|
||||
)
|
||||
}
|
||||
.onNodeWithTextAfterScroll(text = "Fingerprint")
|
||||
.assertExists()
|
||||
.assert(!isEditable())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
|
|
@ -2725,36 +2725,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PublicKeyTextChange should update public key`() = runTest {
|
||||
val action = VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
|
||||
publicKey = "newPublicKey",
|
||||
)
|
||||
val expectedState = createVaultAddItemState(
|
||||
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||
publicKey = "newPublicKey",
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(action)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PrivateKeyTextChange should update private key`() = runTest {
|
||||
val action = VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
|
||||
privateKey = "newPrivateKey",
|
||||
)
|
||||
val expectedState = createVaultAddItemState(
|
||||
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||
privateKey = "newPrivateKey",
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(action)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PrivateKeyVisibilityChange should update private key visibility`() = runTest {
|
||||
val action = VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange(
|
||||
|
@ -2769,21 +2739,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FingerprintTextChange should update fingerprint`() = runTest {
|
||||
val action = VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
|
||||
fingerprint = "newFingerprint",
|
||||
)
|
||||
val expectedState = createVaultAddItemState(
|
||||
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||
fingerprint = "newFingerprint",
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(action)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,12 +7,12 @@ targetSdk = "34"
|
|||
minSdk = "29"
|
||||
|
||||
# Dependency Versions
|
||||
androidGradlePlugin = "8.7.1"
|
||||
androidGradlePlugin = "8.7.2"
|
||||
androidxActivity = "1.9.3"
|
||||
androidXBiometrics = "1.2.0-alpha05"
|
||||
androidxBrowser = "1.8.0"
|
||||
androidxCamera = "1.4.0"
|
||||
androidxComposeBom = "2024.10.00"
|
||||
androidxComposeBom = "2024.10.01"
|
||||
androidxCore = "1.13.1"
|
||||
androidxCredentials = "1.3.0"
|
||||
androidxHiltNavigationCompose = "1.2.0"
|
||||
|
|
Loading…
Add table
Reference in a new issue