diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt
index 71abdba5b..9f451a7ab 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/CipherViewExtensions.kt
@@ -21,7 +21,6 @@ private const val CARD_DIGITS_DISPLAYED: Int = 4
val CipherView.subtitle: String?
get() = when (type) {
CipherType.LOGIN -> this.login?.username.orEmpty()
- CipherType.SECURE_NOTE -> null
CipherType.CARD -> {
this
.card
@@ -45,6 +44,10 @@ val CipherView.subtitle: String?
}
}
}
+
+ CipherType.SECURE_NOTE,
+ CipherType.SSH_KEY,
+ -> null
}
/**
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequest.kt
index dc7adef69..dbf4266f4 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequest.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequest.kt
@@ -51,6 +51,9 @@ data class CipherJsonRequest(
@SerialName("secureNote")
val secureNote: SyncResponseJson.Cipher.SecureNote?,
+ @SerialName("sshKey")
+ val sshKey: SyncResponseJson.Cipher.SshKey?,
+
@SerialName("folderId")
val folderId: String?,
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherTypeJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherTypeJson.kt
index 63ddfaec8..1b43aeec7 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherTypeJson.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherTypeJson.kt
@@ -33,6 +33,12 @@ enum class CipherTypeJson {
*/
@SerialName("4")
IDENTITY,
+
+ /**
+ * A SSH key.
+ */
+ @SerialName("5")
+ SSH_KEY,
}
@Keep
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt
index 5ea91d252..8e47a2624 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt
@@ -472,6 +472,9 @@ data class SyncResponseJson(
@SerialName("identity")
val identity: Identity?,
+ @SerialName("sshKey")
+ val sshKey: SshKey?,
+
@SerialName("collectionIds")
val collectionIds: List?,
@@ -718,6 +721,25 @@ data class SyncResponseJson(
)
}
+ /**
+ * Represents a SSH key in the vault response.
+ *
+ * @property publicKey The public key of the SSH key.
+ * @property privateKey The private key of the SSH key.
+ * @property keyFingerprint The key fingerprint of the SSH key.
+ */
+ @Serializable
+ data class SshKey(
+ @SerialName("publicKey")
+ val publicKey: String?,
+
+ @SerialName("privateKey")
+ val privateKey: String?,
+
+ @SerialName("keyFingerprint")
+ val keyFingerprint: String?,
+ )
+
/**
* Represents password history in the vault response.
*
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt
index 6c5e8f28d..c31cffaad 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt
@@ -17,6 +17,7 @@ import com.bitwarden.vault.LoginUri
import com.bitwarden.vault.PasswordHistory
import com.bitwarden.vault.SecureNote
import com.bitwarden.vault.SecureNoteType
+import com.bitwarden.vault.SshKey
import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
@@ -55,6 +56,7 @@ fun Cipher.toEncryptedNetworkCipher(): CipherJsonRequest =
isFavorite = favorite,
card = card?.toEncryptedNetworkCard(),
key = key,
+ sshKey = sshKey?.toEncryptedNetworkSshKey(),
)
/**
@@ -77,6 +79,7 @@ fun Cipher.toEncryptedNetworkCipherResponse(): SyncResponseJson.Cipher =
isFavorite = favorite,
card = card?.toEncryptedNetworkCard(),
attachments = attachments?.toNetworkAttachmentList(),
+ sshKey = sshKey?.toEncryptedNetworkSshKey(),
shouldOrganizationUseTotp = organizationUseTotp,
shouldEdit = edit,
revisionDate = ZonedDateTime.ofInstant(revisionDate, ZoneOffset.UTC),
@@ -102,6 +105,13 @@ private fun Card.toEncryptedNetworkCard(): SyncResponseJson.Cipher.Card =
brand = brand,
)
+private fun SshKey.toEncryptedNetworkSshKey(): SyncResponseJson.Cipher.SshKey =
+ SyncResponseJson.Cipher.SshKey(
+ publicKey = publicKey,
+ privateKey = privateKey,
+ keyFingerprint = fingerprint,
+ )
+
/**
* Converts a list of Bitwarden SDK [Field] objects to a corresponding
* list of [SyncResponseJson.Cipher.Field] objects.
@@ -309,6 +319,7 @@ private fun CipherType.toNetworkCipherType(): CipherTypeJson =
CipherType.SECURE_NOTE -> CipherTypeJson.SECURE_NOTE
CipherType.CARD -> CipherTypeJson.CARD
CipherType.IDENTITY -> CipherTypeJson.IDENTITY
+ CipherType.SSH_KEY -> CipherTypeJson.SSH_KEY
}
/**
@@ -334,6 +345,7 @@ fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher =
type = type.toSdkCipherType(),
login = login?.toSdkLogin(),
identity = identity?.toSdkIdentity(),
+ sshKey = sshKey?.toSdkSshKey(),
card = card?.toSdkCard(),
secureNote = secureNote?.toSdkSecureNote(),
favorite = isFavorite,
@@ -432,6 +444,17 @@ fun SyncResponseJson.Cipher.SecureNote.toSdkSecureNote(): SecureNote =
},
)
+/**
+ * Transforms a [SyncResponseJson.Cipher.SshKey] into
+ * the corresponding Bitwarden SDK [SshKey].
+ */
+fun SyncResponseJson.Cipher.SshKey.toSdkSshKey(): SshKey =
+ SshKey(
+ publicKey = publicKey,
+ privateKey = privateKey,
+ fingerprint = keyFingerprint,
+ )
+
/**
* Transforms a list of [SyncResponseJson.Cipher.Login.Uri] into
* a corresponding list of Bitwarden SDK [LoginUri].
@@ -517,6 +540,7 @@ fun CipherTypeJson.toSdkCipherType(): CipherType =
CipherTypeJson.SECURE_NOTE -> CipherType.SECURE_NOTE
CipherTypeJson.CARD -> CipherType.CARD
CipherTypeJson.IDENTITY -> CipherType.IDENTITY
+ CipherTypeJson.SSH_KEY -> CipherType.SSH_KEY
}
/**
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt
index 40f59b517..93bf3d9a0 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt
@@ -26,6 +26,7 @@ private const val SEARCH_TYPE_VAULT_TRASH: String = "search_type_vault_trash"
private const val SEARCH_TYPE_VAULT_VERIFICATION_CODES: String =
"search_type_vault_verification_codes"
private const val SEARCH_TYPE_ID: String = "search_type_id"
+private const val SEARCH_TYPE_VAULT_SSH_KEYS: String = "search_type_vault_ssh_keys"
private const val SEARCH_ROUTE_PREFIX: String = "search"
private const val SEARCH_ROUTE: String = "$SEARCH_ROUTE_PREFIX/{$SEARCH_TYPE}/{$SEARCH_TYPE_ID}"
@@ -104,6 +105,7 @@ private fun determineSearchType(
SEARCH_TYPE_VAULT_FOLDER -> SearchType.Vault.Folder(requireNotNull(id))
SEARCH_TYPE_VAULT_TRASH -> SearchType.Vault.Trash
SEARCH_TYPE_VAULT_VERIFICATION_CODES -> SearchType.Vault.VerificationCodes
+ SEARCH_TYPE_VAULT_SSH_KEYS -> SearchType.Vault.SshKeys
else -> throw IllegalArgumentException("Invalid Search Type")
}
@@ -122,6 +124,7 @@ private fun SearchType.toTypeString(): String =
SearchType.Vault.SecureNotes -> SEARCH_TYPE_VAULT_SECURE_NOTES
SearchType.Vault.Trash -> SEARCH_TYPE_VAULT_TRASH
SearchType.Vault.VerificationCodes -> SEARCH_TYPE_VAULT_VERIFICATION_CODES
+ SearchType.Vault.SshKeys -> SEARCH_TYPE_VAULT_SSH_KEYS
}
private fun SearchType.toIdOrNull(): String? =
@@ -139,4 +142,5 @@ private fun SearchType.toIdOrNull(): String? =
SearchType.Vault.SecureNotes -> null
SearchType.Vault.Trash -> null
SearchType.Vault.VerificationCodes -> null
+ SearchType.Vault.SshKeys -> null
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt
index 1f06d7871..f498b8372 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt
@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
+import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -876,6 +877,7 @@ sealed class SearchTypeData : Parcelable {
* Indicates that we should be searching vault items.
*/
@Parcelize
+ @OmitFromCoverage
sealed class Vault : SearchTypeData() {
/**
* Indicates that we should be searching all vault items.
@@ -924,6 +926,16 @@ sealed class SearchTypeData : Parcelable {
.concat(R.string.secure_notes.asText())
}
+ /**
+ * Indicates that we should be searching only ssh key ciphers.
+ */
+ data object SshKeys : Vault() {
+ override val title: Text
+ get() = R.string.search.asText()
+ .concat(" ".asText())
+ .concat(R.string.ssh_keys.asText())
+ }
+
/**
* Indicates that we should be searching only ciphers in the given collection.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt
index 3eb74d887..94331d15a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt
@@ -58,6 +58,11 @@ sealed class SearchType : Parcelable {
*/
data object SecureNotes : Vault()
+ /**
+ * Indicates that we should be searching only SSH key ciphers.
+ */
+ data object SshKeys : Vault()
+
/**
* Indicates that we should be searching only ciphers in the given collection.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt
index 861eb7c67..650dfd707 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt
@@ -62,6 +62,7 @@ fun SearchTypeData.updateWithAdditionalDataIfNecessary(
SearchTypeData.Vault.SecureNotes -> this
SearchTypeData.Vault.Trash -> this
SearchTypeData.Vault.VerificationCodes -> this
+ SearchTypeData.Vault.SshKeys -> this
}
/**
@@ -114,6 +115,7 @@ private fun CipherView.filterBySearchType(
is SearchTypeData.Vault.Identities -> type == CipherType.IDENTITY && deletedDate == null
is SearchTypeData.Vault.Logins -> type == CipherType.LOGIN && deletedDate == null
is SearchTypeData.Vault.SecureNotes -> type == CipherType.SECURE_NOTE && deletedDate == null
+ is SearchTypeData.Vault.SshKeys -> type == CipherType.SSH_KEY && deletedDate == null
is SearchTypeData.Vault.VerificationCodes -> login?.totp != null && deletedDate == null
is SearchTypeData.Vault.Trash -> deletedDate != null
}
@@ -255,6 +257,7 @@ private val CipherType.iconRes: Int
CipherType.SECURE_NOTE -> R.drawable.ic_note
CipherType.CARD -> R.drawable.ic_payment_card
CipherType.IDENTITY -> R.drawable.ic_id_card
+ CipherType.SSH_KEY -> R.drawable.ic_ssh_key
}
/**
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensions.kt
index 29b865ec9..c0475acb2 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensions.kt
@@ -21,4 +21,5 @@ fun SearchType.toSearchTypeData(): SearchTypeData =
SearchType.Vault.SecureNotes -> SearchTypeData.Vault.SecureNotes
SearchType.Vault.Trash -> SearchTypeData.Vault.Trash
SearchType.Vault.VerificationCodes -> SearchTypeData.Vault.VerificationCodes
+ SearchType.Vault.SshKeys -> SearchTypeData.Vault.SshKeys
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt
index 979a021c8..fcd7fc16a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt
@@ -21,21 +21,24 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTyp
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
+import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
import kotlinx.collections.immutable.toImmutableList
/**
* The top level content UI state for the [VaultAddEditScreen].
*/
@Composable
-@Suppress("LongMethod")
+@Suppress("LongMethod", "CyclomaticComplexMethod")
fun VaultAddEditContent(
state: VaultAddEditState.ViewState.Content,
isAddItemMode: Boolean,
+ typeOptions: List,
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
commonTypeHandlers: VaultAddEditCommonHandlers,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
cardItemTypeHandlers: VaultAddEditCardTypeHandlers,
+ sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers,
modifier: Modifier = Modifier,
permissionsManager: PermissionsManager,
) {
@@ -45,6 +48,7 @@ fun VaultAddEditContent(
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Card -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit
+ is VaultAddEditState.ViewState.Content.ItemType.SshKey -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Login -> {
loginItemTypeHandlers.onSetupTotpClick(isGranted)
}
@@ -77,6 +81,7 @@ fun VaultAddEditContent(
item {
Spacer(modifier = Modifier.height(8.dp))
TypeOptionsItem(
+ entries = typeOptions,
itemType = state.type,
onTypeOptionClicked = onTypeOptionClicked,
modifier = Modifier
@@ -131,6 +136,15 @@ fun VaultAddEditContent(
commonTypeHandlers = commonTypeHandlers,
)
}
+
+ is VaultAddEditState.ViewState.Content.ItemType.SshKey -> {
+ vaultAddEditSshKeyItems(
+ commonState = state.common,
+ sshKeyState = state.type,
+ commonTypeHandlers = commonTypeHandlers,
+ sshKeyTypeHandlers = sshKeyItemTypeHandlers,
+ )
+ }
}
item {
@@ -141,12 +155,12 @@ fun VaultAddEditContent(
@Composable
private fun TypeOptionsItem(
+ entries: List,
itemType: VaultAddEditState.ViewState.Content.ItemType,
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
modifier: Modifier = Modifier,
) {
- val possibleMainStates = VaultAddEditState.ItemTypeOption.entries.toList()
- val optionsWithStrings = possibleMainStates.associateWith { stringResource(id = it.labelRes) }
+ val optionsWithStrings = entries.associateWith { stringResource(id = it.labelRes) }
BitwardenMultiSelectButton(
label = stringResource(id = R.string.type),
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt
index 5a9ee185c..ea2ec0fa6 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt
@@ -21,6 +21,7 @@ private const val LOGIN: String = "login"
private const val CARD: String = "card"
private const val IDENTITY: String = "identity"
private const val SECURE_NOTE: String = "secure_note"
+private const val SSH_KEY: String = "ssh_key"
private const val ADD_ITEM_TYPE: String = "vault_add_item_type"
private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item"
@@ -127,6 +128,7 @@ private fun VaultItemCipherType.toTypeString(): String =
VaultItemCipherType.CARD -> CARD
VaultItemCipherType.IDENTITY -> IDENTITY
VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE
+ VaultItemCipherType.SSH_KEY -> SSH_KEY
}
private fun String.toVaultItemCipherType(): VaultItemCipherType =
@@ -135,6 +137,7 @@ private fun String.toVaultItemCipherType(): VaultItemCipherType =
CARD -> VaultItemCipherType.CARD
IDENTITY -> VaultItemCipherType.IDENTITY
SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE
+ SSH_KEY -> VaultItemCipherType.SSH_KEY
else -> throw IllegalStateException(
"Edit Item string arguments for VaultAddEditNavigation must match!",
)
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt
index 092806bd2..2bf3d550c 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt
@@ -58,6 +58,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTyp
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
+import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditUserVerificationHandlers
/**
@@ -155,6 +156,10 @@ fun VaultAddEditScreen(
VaultAddEditCardTypeHandlers.create(viewModel = viewModel)
}
+ val sshKeyItemTypeHandlers = remember(viewModel) {
+ VaultAddEditSshKeyTypeHandlers.create(viewModel = viewModel)
+ }
+
val confirmDeleteClickAction = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.ConfirmDeleteClick) }
}
@@ -321,6 +326,7 @@ fun VaultAddEditScreen(
VaultAddEditContent(
state = viewState,
isAddItemMode = state.isAddItemMode,
+ typeOptions = state.supportedItemTypes,
onTypeOptionClicked = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.TypeOptionSelect(it)) }
},
@@ -329,6 +335,7 @@ fun VaultAddEditScreen(
permissionsManager = permissionsManager,
identityItemTypeHandlers = identityItemTypeHandlers,
cardItemTypeHandlers = cardItemTypeHandlers,
+ sshKeyItemTypeHandlers = sshKeyItemTypeHandlers,
modifier = Modifier
.imePadding()
.padding(innerPadding)
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSshKeyItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSshKeyItems.kt
new file mode 100644
index 000000000..547ea390d
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSshKeyItems.kt
@@ -0,0 +1,127 @@
+package com.x8bit.bitwarden.ui.vault.feature.addedit
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+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.field.BitwardenPasswordField
+import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
+import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
+import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
+import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
+
+/**
+ * The UI for adding and editing a SSH key cipher.
+ */
+fun LazyListScope.vaultAddEditSshKeyItems(
+ commonState: VaultAddEditState.ViewState.Content.Common,
+ sshKeyState: VaultAddEditState.ViewState.Content.ItemType.SshKey,
+ commonTypeHandlers: VaultAddEditCommonHandlers,
+ sshKeyTypeHandlers: VaultAddEditSshKeyTypeHandlers,
+) {
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenTextField(
+ label = stringResource(id = R.string.name),
+ value = commonState.name,
+ onValueChange = commonTypeHandlers.onNameTextChange,
+ modifier = Modifier
+ .testTag("ItemNameEntry")
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenTextField(
+ label = stringResource(id = R.string.public_key),
+ value = sshKeyState.publicKey,
+ onValueChange = sshKeyTypeHandlers.onPublicKeyTextChange,
+ modifier = Modifier
+ .testTag("PublicKeyEntry")
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenPasswordField(
+ label = stringResource(id = R.string.private_key),
+ value = sshKeyState.privateKey,
+ onValueChange = sshKeyTypeHandlers.onPrivateKeyTextChange,
+ showPassword = sshKeyState.showPrivateKey,
+ showPasswordChange = { sshKeyTypeHandlers.onPrivateKeyVisibilityChange(it) },
+ showPasswordTestTag = "ViewPrivateKeyButton",
+ modifier = Modifier
+ .testTag("PrivateKeyEntry")
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenTextField(
+ label = stringResource(id = R.string.fingerprint),
+ value = sshKeyState.fingerprint,
+ onValueChange = sshKeyTypeHandlers.onFingerprintTextChange,
+ modifier = Modifier
+ .testTag("FingerprintEntry")
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun VaultAddEditSshKeyItems_preview() {
+ BitwardenTheme {
+ LazyColumn {
+ vaultAddEditSshKeyItems(
+ commonState = VaultAddEditState.ViewState.Content.Common(
+ name = "SSH Key",
+ ),
+ sshKeyState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
+ publicKey = "public key",
+ privateKey = "private key",
+ fingerprint = "fingerprint",
+ showPublicKey = false,
+ showPrivateKey = false,
+ showFingerprint = false,
+ ),
+ commonTypeHandlers = VaultAddEditCommonHandlers(
+ onNameTextChange = { },
+ onFolderSelected = { },
+ onToggleFavorite = { },
+ onToggleMasterPasswordReprompt = { },
+ onNotesTextChange = { },
+ onOwnerSelected = { },
+ onTooltipClick = { },
+ onAddNewCustomFieldClick = { _, _ -> },
+ onCustomFieldValueChange = { },
+ onCustomFieldActionSelect = { _, _ -> },
+ onCollectionSelect = { },
+ onHiddenFieldVisibilityChange = { },
+ ),
+ sshKeyTypeHandlers = VaultAddEditSshKeyTypeHandlers(
+ onPublicKeyTextChange = { },
+ onPrivateKeyTextChange = { },
+ onPrivateKeyVisibilityChange = { },
+ onFingerprintTextChange = { },
+ ),
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt
index 1aa9daa52..3c2e0b6e3 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt
@@ -16,10 +16,12 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
@@ -101,6 +103,7 @@ class VaultAddEditViewModel @Inject constructor(
private val resourceManager: ResourceManager,
private val clock: Clock,
private val organizationEventManager: OrganizationEventManager,
+ private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE]
@@ -162,6 +165,11 @@ class VaultAddEditViewModel @Inject constructor(
// Set special conditions for autofill and fido2 save
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
shouldExitOnSave = shouldExitOnSave,
+ supportedItemTypes = getSupportedItemTypeOptions(
+ isSshKeyVaultItemSupported = featureFlagManager.getFeatureFlag(
+ key = FlagKey.SshKeyCipherItems,
+ ),
+ ),
)
},
) {
@@ -203,6 +211,11 @@ class VaultAddEditViewModel @Inject constructor(
}
.onEach(::sendAction)
.launchIn(viewModelScope)
+
+ featureFlagManager.getFeatureFlagFlow(FlagKey.SshKeyCipherItems)
+ .map { VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(it) }
+ .onEach(::sendAction)
+ .launchIn(viewModelScope)
}
override fun handleAction(action: VaultAddEditAction) {
@@ -211,6 +224,7 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.ItemType.LoginType -> handleAddLoginTypeAction(action)
is VaultAddEditAction.ItemType.IdentityType -> handleIdentityTypeActions(action)
is VaultAddEditAction.ItemType.CardType -> handleCardTypeActions(action)
+ is VaultAddEditAction.ItemType.SshKeyType -> handleSshKeyTypeActions(action)
is VaultAddEditAction.Internal -> handleInternalActions(action)
}
}
@@ -320,6 +334,7 @@ class VaultAddEditViewModel @Inject constructor(
VaultAddEditState.ItemTypeOption.CARD -> handleSwitchToAddCardItem()
VaultAddEditState.ItemTypeOption.IDENTITY -> handleSwitchToAddIdentityItem()
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> handleSwitchToAddSecureNotesItem()
+ VaultAddEditState.ItemTypeOption.SSH_KEYS -> handleSwitchToSshKeyItem()
}
}
@@ -371,6 +386,18 @@ class VaultAddEditViewModel @Inject constructor(
}
}
+ private fun handleSwitchToSshKeyItem() {
+ updateContent { currentContent ->
+ currentContent.copy(
+ common = currentContent.clearNonSharedData(),
+ type = currentContent.previousItemTypeOrDefault(
+ itemType = VaultAddEditState.ItemTypeOption.SSH_KEYS,
+ ),
+ previousItemTypes = currentContent.toUpdatedPreviousItemTypes(),
+ )
+ }
+ }
+
@Suppress("LongMethod")
private fun handleSaveClick() = onContent { content ->
if (content.common.name.isBlank()) {
@@ -1363,6 +1390,54 @@ class VaultAddEditViewModel @Inject constructor(
//endregion Card Type Handlers
+ //region SSH Key Type Handlers
+
+ 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
private fun handleInternalActions(action: VaultAddEditAction.Internal) {
@@ -1397,6 +1472,10 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.Internal.ValidateFido2PinResultReceive -> {
handleValidateFido2PinResultReceive(action)
}
+
+ is VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive -> {
+ handleSshKeyCipherItemsFeatureFlagReceive(action)
+ }
}
}
@@ -1707,6 +1786,18 @@ class VaultAddEditViewModel @Inject constructor(
getRequestAndRegisterCredential()
}
+ private fun handleSshKeyCipherItemsFeatureFlagReceive(
+ action: VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive,
+ ) {
+ mutableStateFlow.update {
+ it.copy(
+ supportedItemTypes = getSupportedItemTypeOptions(
+ isSshKeyVaultItemSupported = action.enabled,
+ ),
+ )
+ }
+ }
+
//endregion Internal Type Handlers
//region Utility Functions
@@ -1818,6 +1909,19 @@ class VaultAddEditViewModel @Inject constructor(
}
}
+ private inline fun updateSshKeyContent(
+ crossinline block: (VaultAddEditState.ViewState.Content.ItemType.SshKey) ->
+ VaultAddEditState.ViewState.Content.ItemType.SshKey,
+ ) {
+ updateContent { currentContent ->
+ (currentContent.type as? VaultAddEditState.ViewState.Content.ItemType.SshKey)?.let {
+ currentContent.copy(
+ type = block(it),
+ )
+ }
+ }
+ }
+
private fun VaultAddEditState.ViewState.Content.clearNonSharedData():
VaultAddEditState.ViewState.Content.Common =
common.copy(
@@ -1852,6 +1956,10 @@ class VaultAddEditViewModel @Inject constructor(
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> {
VaultAddEditState.ViewState.Content.ItemType.SecureNotes
}
+
+ VaultAddEditState.ItemTypeOption.SSH_KEYS -> {
+ VaultAddEditState.ViewState.Content.ItemType.SshKey()
+ }
},
)
@@ -1911,6 +2019,7 @@ data class VaultAddEditState(
val viewState: ViewState,
val dialog: DialogState?,
val shouldShowCloseButton: Boolean = true,
+ val supportedItemTypes: List,
// Internal
val shouldExitOnSave: Boolean = false,
val totpData: TotpData? = null,
@@ -1957,6 +2066,7 @@ data class VaultAddEditState(
CARD(R.string.type_card),
IDENTITY(R.string.type_identity),
SECURE_NOTES(R.string.type_secure_note),
+ SSH_KEYS(R.string.type_ssh_key),
}
/**
@@ -2162,6 +2272,25 @@ data class VaultAddEditState(
data object SecureNotes : ItemType() {
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SECURE_NOTES
}
+
+ /**
+ * Represents the `SshKey` item type.
+ *
+ * @property publicKey The public key for the SSH key item.
+ * @property privateKey The private key for the SSH key item.
+ * @property fingerprint The fingerprint for the SSH key item.
+ */
+ @Parcelize
+ data class SshKey(
+ val publicKey: String = "",
+ val privateKey: String = "",
+ val fingerprint: String = "",
+ val showPublicKey: Boolean = false,
+ val showPrivateKey: Boolean = false,
+ val showFingerprint: Boolean = false,
+ ) : ItemType() {
+ override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SSH_KEYS
+ }
}
/**
@@ -2928,6 +3057,31 @@ sealed class VaultAddEditAction {
*/
data class SecurityCodeVisibilityChange(val isVisible: Boolean) : CardType()
}
+
+ /**
+ * 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()
+ }
}
/**
@@ -2952,6 +3106,13 @@ sealed class VaultAddEditAction {
val generatorResult: GeneratorResult,
) : Internal()
+ /**
+ * Indicates that the the SSH key vault item feature flag state has been received.
+ */
+ data class SshKeyCipherItemsFeatureFlagReceive(
+ val enabled: Boolean,
+ ) : Internal()
+
/**
* Indicates that the vault item data has been received.
*/
@@ -3005,3 +3166,8 @@ sealed class VaultAddEditAction {
) : Internal()
}
}
+
+private fun getSupportedItemTypeOptions(
+ isSshKeyVaultItemSupported: Boolean,
+) = VaultAddEditState.ItemTypeOption.entries
+ .filter { isSshKeyVaultItemSupported || it != VaultAddEditState.ItemTypeOption.SSH_KEYS }
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditSshKeyTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditSshKeyTypeHandlers.kt
new file mode 100644
index 000000000..3fb197d9c
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditSshKeyTypeHandlers.kt
@@ -0,0 +1,64 @@
+package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers
+
+import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction
+import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
+
+/**
+ * Provides a set of handlers for interactions related to SSH key types within the vault add/edit
+ * screen.
+ *
+ * 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 {
+ /**
+ * Creates an instance of [VaultAddEditSshKeyTypeHandlers] with handlers that dispatch
+ * actions to the provided ViewModel.
+ *
+ * @param viewModel The ViewModel to which actions will be dispatched.
+ */
+ 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(
+ isVisible = it,
+ ),
+ )
+ },
+ onFingerprintTextChange = { newFingerprint ->
+ viewModel.trySendAction(
+ VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
+ fingerprint = newFingerprint,
+ ),
+ )
+ },
+ )
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt
index 70306669c..0163a1b7e 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/CustomFieldType.kt
@@ -66,4 +66,5 @@ private val VaultAddEditState.ViewState.Content.ItemType.defaultLinkedFieldTypeO
is VaultAddEditState.ViewState.Content.ItemType.Identity -> VaultLinkedFieldType.TITLE
is VaultAddEditState.ViewState.Content.ItemType.Login -> VaultLinkedFieldType.USERNAME
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> null
+ is VaultAddEditState.ViewState.Content.ItemType.SshKey -> null
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt
index 62af2170e..749e8e051 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt
@@ -35,6 +35,7 @@ private const val PASSKEY_CREATION_TIME_PATTERN: String = "hh:mm a"
/**
* Transforms [CipherView] into [VaultAddEditState.ViewState].
*/
+@Suppress("LongMethod")
fun CipherView.toViewState(
isClone: Boolean,
isIndividualVaultDisabled: Boolean,
@@ -88,6 +89,12 @@ fun CipherView.toViewState(
zip = identity?.postalCode.orEmpty(),
country = identity?.country.orEmpty(),
)
+
+ CipherType.SSH_KEY -> VaultAddEditState.ViewState.Content.ItemType.SshKey(
+ publicKey = sshKey?.publicKey.orEmpty(),
+ privateKey = sshKey?.privateKey.orEmpty(),
+ fingerprint = sshKey?.fingerprint.orEmpty(),
+ )
},
common = VaultAddEditState.ViewState.Content.Common(
originalCipher = this,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt
index 807c43f22..7a637b6bd 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensions.kt
@@ -25,4 +25,5 @@ fun VaultItemCipherType.toItemType(): VaultAddEditState.ViewState.Content.ItemTy
VaultItemCipherType.CARD -> VaultAddEditState.ViewState.Content.ItemType.Card()
VaultItemCipherType.IDENTITY -> VaultAddEditState.ViewState.Content.ItemType.Identity()
VaultItemCipherType.SECURE_NOTE -> VaultAddEditState.ViewState.Content.ItemType.SecureNotes
+ VaultItemCipherType.SSH_KEY -> VaultAddEditState.ViewState.Content.ItemType.SshKey()
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt
index 38965e384..63011234f 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt
@@ -45,6 +45,7 @@ import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
+import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
/**
* Displays the vault item screen.
@@ -265,6 +266,9 @@ fun VaultItemScreen(
vaultCardItemTypeHandlers = remember(viewModel) {
VaultCardItemTypeHandlers.create(viewModel = viewModel)
},
+ vaultSshKeyItemTypeHandlers = remember(viewModel) {
+ VaultSshKeyItemTypeHandlers.create(viewModel = viewModel)
+ },
)
}
}
@@ -342,6 +346,7 @@ private fun VaultItemContent(
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
+ vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
modifier: Modifier = Modifier,
) {
when (viewState) {
@@ -389,6 +394,15 @@ private fun VaultItemContent(
modifier = modifier,
)
}
+
+ is VaultItemState.ViewState.Content.ItemType.SshKey -> {
+ VaultItemSshKeyContent(
+ commonState = viewState.common,
+ sshKeyItemState = viewState.type,
+ vaultSshKeyItemTypeHandlers = vaultSshKeyItemTypeHandlers,
+ modifier = modifier,
+ )
+ }
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt
new file mode 100644
index 000000000..bac0d17c5
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt
@@ -0,0 +1,127 @@
+package com.x8bit.bitwarden.ui.vault.feature.item
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+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.components.field.BitwardenPasswordField
+import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
+import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
+import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
+
+/**
+ * The top level content UI state for the [VaultItemScreen] when viewing a SSH key cipher.
+ */
+@Suppress("LongMethod")
+@Composable
+fun VaultItemSshKeyContent(
+ commonState: VaultItemState.ViewState.Content.Common,
+ sshKeyItemState: VaultItemState.ViewState.Content.ItemType.SshKey,
+ vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(modifier = modifier) {
+ item {
+ BitwardenListHeaderText(
+ label = stringResource(id = R.string.item_information),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenTextField(
+ label = stringResource(id = R.string.name),
+ value = commonState.name,
+ onValueChange = { },
+ readOnly = true,
+ singleLine = false,
+ modifier = Modifier
+ .testTag("SshKeyItemNameEntry")
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ )
+ }
+
+ sshKeyItemState.publicKey?.let { publicKey ->
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenTextField(
+ label = stringResource(id = R.string.public_key),
+ value = publicKey,
+ onValueChange = { },
+ singleLine = false,
+ readOnly = true,
+ modifier = Modifier
+ .testTag("SshKeyItemPublicKeyEntry")
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ )
+ }
+ }
+
+ sshKeyItemState.privateKey?.let { privateKey ->
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenPasswordField(
+ label = stringResource(id = R.string.private_key),
+ value = privateKey,
+ onValueChange = { },
+ singleLine = false,
+ readOnly = true,
+ showPassword = sshKeyItemState.showPrivateKey,
+ showPasswordTestTag = "ViewPrivateKeyButton",
+ showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,
+ modifier = Modifier
+ .testTag("SshKeyItemPrivateKeyEntry")
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ )
+ }
+ }
+
+ sshKeyItemState.fingerprint?.let { fingerprint ->
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ BitwardenTextField(
+ label = stringResource(id = R.string.fingerprint),
+ value = fingerprint,
+ onValueChange = { },
+ singleLine = false,
+ readOnly = true,
+ modifier = Modifier
+ .testTag("SshKeyItemFingerprintEntry")
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ )
+ }
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(24.dp))
+ VaultItemUpdateText(
+ header = "${stringResource(id = R.string.date_updated)}: ",
+ text = commonState.lastUpdated,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .testTag("SshKeyItemLastUpdated"),
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(88.dp))
+ Spacer(modifier = Modifier.navigationBarsPadding())
+ }
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt
index df774ff66..72063980c 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt
@@ -116,6 +116,7 @@ class VaultItemViewModel @Inject constructor(
when (action) {
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
+ is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action)
is VaultItemAction.Common -> handleCommonActions(action)
is VaultItemAction.Internal -> handleInternalAction(action)
}
@@ -753,6 +754,32 @@ class VaultItemViewModel @Inject constructor(
//endregion Card Type Handlers
+ //region SSH Key Type Handlers
+
+ private fun handleSshKeyTypeActions(action: VaultItemAction.ItemType.SshKey) {
+ when (action) {
+ is VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked -> {
+ handlePrivateKeyVisibilityClicked(action)
+ }
+ }
+ }
+
+ private fun handlePrivateKeyVisibilityClicked(
+ action: VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked,
+ ) {
+ onSshKeyContent { content, sshKey ->
+ mutableStateFlow.update { currentState ->
+ currentState.copy(
+ viewState = content.copy(
+ type = sshKey.copy(showPrivateKey = action.isVisible),
+ ),
+ )
+ }
+ }
+ }
+
+ //endregion SSH Key Type Handlers
+
//region Internal Type Handlers
private fun handleInternalAction(action: VaultItemAction.Internal) {
@@ -1057,6 +1084,21 @@ class VaultItemViewModel @Inject constructor(
}
}
}
+
+ private inline fun onSshKeyContent(
+ crossinline block: (
+ VaultItemState.ViewState.Content,
+ VaultItemState.ViewState.Content.ItemType.SshKey,
+ ) -> Unit,
+ ) {
+ state.viewState.asContentOrNull()
+ ?.let { content ->
+ (content.type as? VaultItemState.ViewState.Content.ItemType.SshKey)
+ ?.let { sshKeyContent ->
+ block(content, sshKeyContent)
+ }
+ }
+ }
}
/**
@@ -1359,6 +1401,20 @@ data class VaultItemState(
val isVisible: Boolean,
) : Parcelable
}
+
+ /**
+ * Represents the data for displaying an `SSHKey` item type.
+ *
+ * @property name The name of the key.
+ * @property privateKey The SSH private key.
+ */
+ data class SshKey(
+ val name: String?,
+ val publicKey: String?,
+ val privateKey: String?,
+ val fingerprint: String?,
+ val showPrivateKey: Boolean,
+ ) : ItemType()
}
}
@@ -1697,6 +1753,16 @@ sealed class VaultItemAction {
*/
data class NumberVisibilityClick(val isVisible: Boolean) : Card()
}
+
+ /**
+ * Represents actions specific to the SshKey type.
+ */
+ sealed class SshKey : ItemType() {
+ /**
+ * The user has clicked to display the private key.
+ */
+ data class PrivateKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
+ }
}
/**
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultSshKeyItemTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultSshKeyItemTypeHandlers.kt
new file mode 100644
index 000000000..fc4e1411b
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultSshKeyItemTypeHandlers.kt
@@ -0,0 +1,32 @@
+package com.x8bit.bitwarden.ui.vault.feature.item.handlers
+
+import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
+import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
+
+/**
+ * A collection of handler functions for managing actions within the context of viewing SSH key
+ * items in a vault.
+ */
+data class VaultSshKeyItemTypeHandlers(
+ val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
+) {
+
+ @Suppress("UndocumentedPublicClass")
+ companion object {
+
+ /**
+ * Creates the [VaultSshKeyItemTypeHandlers] using the [viewModel] to send desired actions.
+ */
+ @Suppress("LongMethod")
+ fun create(viewModel: VaultItemViewModel): VaultSshKeyItemTypeHandlers =
+ VaultSshKeyItemTypeHandlers(
+ onShowPrivateKeyClick = {
+ viewModel.trySendAction(
+ VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
+ isVisible = it,
+ ),
+ )
+ },
+ )
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt
index 108e1cb76..e4c46f592 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt
@@ -156,6 +156,19 @@ fun CipherView.toViewState(
address = identity?.identityAddress,
)
}
+
+ CipherType.SSH_KEY -> {
+ val sshKeyValues = requireNotNull(sshKey)
+ VaultItemState.ViewState.Content.ItemType.SshKey(
+ name = name,
+ publicKey = sshKeyValues.publicKey,
+ privateKey = sshKeyValues.privateKey,
+ fingerprint = sshKeyValues.fingerprint,
+ showPrivateKey = (previousState?.type as?
+ VaultItemState.ViewState.Content.ItemType.SshKey)
+ ?.showPrivateKey == true,
+ )
+ }
},
)
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt
index daf653fed..b076c532a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt
@@ -18,6 +18,7 @@ private const val COLLECTION: String = "collection"
private const val FOLDER: String = "folder"
private const val IDENTITY: String = "identity"
private const val LOGIN: String = "login"
+private const val SSH_KEY: String = "ssh_key"
private const val SECURE_NOTE: String = "secure_note"
private const val SEND_FILE: String = "send_file"
private const val SEND_TEXT: String = "send_text"
@@ -234,6 +235,7 @@ private fun VaultItemListingType.toTypeString(): String {
is VaultItemListingType.Trash -> TRASH
is VaultItemListingType.SendFile -> SEND_FILE
is VaultItemListingType.SendText -> SEND_TEXT
+ is VaultItemListingType.SshKey -> SSH_KEY
}
}
@@ -248,6 +250,7 @@ private fun VaultItemListingType.toIdOrNull(): String? =
is VaultItemListingType.Trash -> null
is VaultItemListingType.SendFile -> null
is VaultItemListingType.SendText -> null
+ is VaultItemListingType.SshKey -> null
}
private fun determineVaultItemListingType(
@@ -259,6 +262,7 @@ private fun determineVaultItemListingType(
CARD -> VaultItemListingType.Card
IDENTITY -> VaultItemListingType.Identity
SECURE_NOTE -> VaultItemListingType.SecureNote
+ SSH_KEY -> VaultItemListingType.SshKey
TRASH -> VaultItemListingType.Trash
FOLDER -> VaultItemListingType.Folder(folderId = id)
COLLECTION -> VaultItemListingType.Collection(collectionId = requireNotNull(id))
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt
index d0db9cb6e..e1e8084ef 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt
@@ -2058,6 +2058,14 @@ data class VaultItemListingState(
override val hasFab: Boolean get() = true
}
+ /**
+ * A SSH key item listing.
+ */
+ data object SshKey : Vault() {
+ override val titleText: Text get() = R.string.ssh_keys.asText()
+ override val hasFab: Boolean get() = true
+ }
+
/**
* A Secure Trash item listing.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt
index 148dc3cf2..dfc1f4fcf 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt
@@ -70,6 +70,10 @@ fun CipherView.determineListingPredicate(
type == CipherType.SECURE_NOTE && deletedDate == null
}
+ is VaultItemListingState.ItemListingType.Vault.SshKey -> {
+ type == CipherType.SSH_KEY && deletedDate == null
+ }
+
is VaultItemListingState.ItemListingType.Vault.Trash -> {
deletedDate != null
}
@@ -272,6 +276,7 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
is VaultItemListingState.ItemListingType.Vault.Trash -> this
is VaultItemListingState.ItemListingType.Send.SendFile -> this
is VaultItemListingState.ItemListingType.Send.SendText -> this
+ is VaultItemListingState.ItemListingType.Vault.SshKey -> this
}
@Suppress("LongParameterList")
@@ -374,6 +379,7 @@ private fun CipherView.toIconTestTag(): String =
CipherType.SECURE_NOTE -> "SecureNoteCipherIcon"
CipherType.CARD -> "CardCipherIcon"
CipherType.IDENTITY -> "IdentityCipherIcon"
+ CipherType.SSH_KEY -> "SshKeyCipherIcon"
}
private fun CipherView.toIconData(
@@ -431,4 +437,5 @@ private val CipherType.iconRes: Int
CipherType.SECURE_NOTE -> R.drawable.ic_note
CipherType.CARD -> R.drawable.ic_payment_card
CipherType.IDENTITY -> R.drawable.ic_id_card
+ CipherType.SSH_KEY -> R.drawable.ic_ssh_key
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt
index 3548da9f3..87814f044 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt
@@ -19,6 +19,7 @@ fun VaultItemListingState.ItemListingType.toSearchType(): SearchType =
is VaultItemListingState.ItemListingType.Vault.Identity -> SearchType.Vault.Identities
is VaultItemListingState.ItemListingType.Vault.Login -> SearchType.Vault.Logins
is VaultItemListingState.ItemListingType.Vault.SecureNote -> SearchType.Vault.SecureNotes
+ is VaultItemListingState.ItemListingType.Vault.SshKey -> SearchType.Vault.SshKeys
is VaultItemListingState.ItemListingType.Vault.Trash -> SearchType.Vault.Trash
is VaultItemListingState.ItemListingType.Vault.Collection -> {
SearchType.Vault.Collection(collectionId = collectionId)
@@ -36,6 +37,7 @@ fun VaultItemListingState.ItemListingType.Vault.toVaultItemCipherType(): VaultIt
is VaultItemListingState.ItemListingType.Vault.Card -> VaultItemCipherType.CARD
is VaultItemListingState.ItemListingType.Vault.Identity -> VaultItemCipherType.IDENTITY
is VaultItemListingState.ItemListingType.Vault.SecureNote -> VaultItemCipherType.SECURE_NOTE
+ is VaultItemListingState.ItemListingType.Vault.SshKey -> VaultItemCipherType.SSH_KEY
is VaultItemListingState.ItemListingType.Vault.Login -> VaultItemCipherType.LOGIN
is VaultItemListingState.ItemListingType.Vault.Collection -> VaultItemCipherType.LOGIN
is VaultItemListingState.ItemListingType.Vault.Trash,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensions.kt
index 86a99c31b..dd052de8e 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensions.kt
@@ -16,6 +16,7 @@ fun VaultItemListingType.toItemListingType(): VaultItemListingState.ItemListingT
is VaultItemListingType.Identity -> VaultItemListingState.ItemListingType.Vault.Identity
is VaultItemListingType.Login -> VaultItemListingState.ItemListingType.Vault.Login
is VaultItemListingType.SecureNote -> VaultItemListingState.ItemListingType.Vault.SecureNote
+ is VaultItemListingType.SshKey -> VaultItemListingState.ItemListingType.Vault.SshKey
is VaultItemListingType.Trash -> VaultItemListingState.ItemListingType.Vault.Trash
is VaultItemListingType.Collection -> {
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = collectionId)
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt
index 5e11a4c53..d40429e9b 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt
@@ -31,6 +31,7 @@ fun VaultContent(
state: VaultState.ViewState.Content,
vaultHandlers: VaultHandlers,
onOverflowOptionClick: (action: ListingItemOverflowAction.VaultAction) -> Unit,
+ showSshKeys: Boolean,
modifier: Modifier = Modifier,
) {
LazyColumn(
@@ -122,7 +123,7 @@ fun VaultContent(
item {
BitwardenListHeaderText(
label = stringResource(id = R.string.types),
- supportingLabel = "4",
+ supportingLabel = state.itemTypesCount.toString(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
@@ -193,6 +194,23 @@ fun VaultContent(
)
}
+ if (showSshKeys) {
+ item {
+ BitwardenGroupItem(
+ startIcon = rememberVectorPainter(id = R.drawable.ic_ssh_key),
+ startIconTestTag = "SshKeyCipherIcon",
+ label = stringResource(id = R.string.type_ssh_key),
+ supportingLabel = state.sshKeyItemsCount.toString(),
+ onClick = vaultHandlers.sshKeyGroupClick,
+ showDivider = false,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("SshKeyFilter")
+ .padding(horizontal = 16.dp),
+ )
+ }
+ }
+
if (state.folderItems.isNotEmpty()) {
item {
BitwardenHorizontalDivider(
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt
index 9d165acc8..0bbded82d 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt
@@ -321,6 +321,7 @@ private fun VaultScreenScaffold(
when (val viewState = state.viewState) {
is VaultState.ViewState.Content -> VaultContent(
state = viewState,
+ showSshKeys = state.showSshKeys,
vaultHandlers = vaultHandlers,
onOverflowOptionClick = { masterPasswordRepromptAction = it },
modifier = innerModifier,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
index 526898b32..37a321337 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
@@ -89,6 +89,7 @@ class VaultViewModel @Inject constructor(
.any(),
)
val appBarTitle = vaultFilterData.toAppBarTitle()
+ val showSshKeys = featureFlagManager.getFeatureFlag(FlagKey.SshKeyCipherItems)
VaultState(
appBarTitle = appBarTitle,
initials = activeAccountSummary.initials,
@@ -104,6 +105,7 @@ class VaultViewModel @Inject constructor(
hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
isRefreshing = false,
showImportActionCard = false,
+ showSshKeys = showSshKeys,
)
},
) {
@@ -131,9 +133,16 @@ class VaultViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
- vaultRepository
- .vaultDataStateFlow
- .onEach { sendAction(VaultAction.Internal.VaultDataReceive(vaultData = it)) }
+ combine(
+ vaultRepository.vaultDataStateFlow,
+ featureFlagManager.getFeatureFlagFlow(FlagKey.SshKeyCipherItems),
+ ) { vaultData, sshKeyCipherItemsEnabled ->
+ VaultAction.Internal.VaultDataReceive(
+ vaultData = vaultData,
+ showSshKeys = sshKeyCipherItemsEnabled,
+ )
+ }
+ .onEach(::sendAction)
.launchIn(viewModelScope)
authRepository
@@ -177,6 +186,7 @@ class VaultViewModel @Inject constructor(
is VaultAction.ExitConfirmationClick -> handleExitConfirmationClick()
is VaultAction.VaultFilterTypeSelect -> handleVaultFilterTypeSelect(action)
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
+ is VaultAction.SshKeyGroupClick -> handleSshKeyClick()
is VaultAction.TrashClick -> handleTrashClick()
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
is VaultAction.TryAgainClick -> handleTryAgainClick()
@@ -211,7 +221,10 @@ class VaultViewModel @Inject constructor(
it.copy(isIconLoadingDisabled = action.isIconLoadingDisabled)
}
- updateViewState(vaultRepository.vaultDataStateFlow.value)
+ updateViewState(
+ vaultData = vaultRepository.vaultDataStateFlow.value,
+ showSshKeys = state.showSshKeys,
+ )
}
//region VaultAction Handlers
@@ -311,7 +324,10 @@ class VaultViewModel @Inject constructor(
}
// Re-process the current vault data with the new filter
- updateViewState(vaultData = vaultRepository.vaultDataStateFlow.value)
+ updateViewState(
+ vaultData = vaultRepository.vaultDataStateFlow.value,
+ showSshKeys = state.showSshKeys,
+ )
}
private fun handleTrashClick() {
@@ -322,6 +338,10 @@ class VaultViewModel @Inject constructor(
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.SecureNote))
}
+ private fun handleSshKeyClick() {
+ sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.SshKey))
+ }
+
private fun handleVaultItemClick(action: VaultAction.VaultItemClick) {
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
}
@@ -517,6 +537,7 @@ class VaultViewModel @Inject constructor(
val appBarTitle = vaultFilterData.toAppBarTitle()
val shouldShowImportActionCard = action.importLoginsFlowEnabled &&
firstTimeState.showImportLoginsCard
+
mutableStateFlow.update {
val accountSummaries = userState.toAccountSummaries()
val activeAccountSummary = userState.toActiveAccountSummary()
@@ -537,13 +558,20 @@ class VaultViewModel @Inject constructor(
// navigating.
if (state.isSwitchingAccounts) return
- updateViewState(vaultData = action.vaultData)
+ updateViewState(
+ vaultData = action.vaultData,
+ showSshKeys = action.showSshKeys,
+ )
}
- private fun updateViewState(vaultData: DataState) {
+ private fun updateViewState(vaultData: DataState, showSshKeys: Boolean) {
when (vaultData) {
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
- is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
+ is DataState.Loaded -> vaultLoadedReceive(
+ vaultData = vaultData,
+ showSshKeys = showSshKeys,
+ )
+
is DataState.Loading -> vaultLoadingReceive()
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData = vaultData)
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
@@ -564,7 +592,7 @@ class VaultViewModel @Inject constructor(
)
}
- private fun vaultLoadedReceive(vaultData: DataState.Loaded) {
+ private fun vaultLoadedReceive(vaultData: DataState.Loaded, showSshKeys: Boolean) {
if (state.dialog == VaultState.DialogState.Syncing) {
sendEvent(
VaultEvent.ShowToast(
@@ -580,9 +608,11 @@ class VaultViewModel @Inject constructor(
isPremium = state.isPremium,
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
+ showSshKeys = showSshKeys,
),
dialog = null,
isRefreshing = false,
+ showSshKeys = showSshKeys,
)
}
}
@@ -614,6 +644,7 @@ class VaultViewModel @Inject constructor(
isPremium = state.isPremium,
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
+ showSshKeys = state.showSshKeys,
),
)
}
@@ -685,6 +716,7 @@ data class VaultState(
val hideNotificationsDialog: Boolean,
val isRefreshing: Boolean,
val showImportActionCard: Boolean,
+ val showSshKeys: Boolean,
) : Parcelable {
/**
@@ -767,11 +799,13 @@ data class VaultState(
*/
@Parcelize
data class Content(
+ val itemTypesCount: Int,
val totpItemsCount: Int,
val loginItemsCount: Int,
val cardItemsCount: Int,
val identityItemsCount: Int,
val secureNoteItemsCount: Int,
+ val sshKeyItemsCount: Int,
val favoriteItems: List,
val folderItems: List,
val noFolderItems: List,
@@ -944,6 +978,29 @@ data class VaultState(
) : VaultItem() {
override val supportingLabel: Text? get() = null
}
+
+ /**
+ * Represents a SSH key item within the vault, designed to store SSH keys.
+ *
+ * @property publicKey The public key associated with this SSH key item.
+ * @property privateKey The private key associated with this SSH key item.
+ * @property fingerprint The fingerprint associated with this SSH key item.
+ */
+ @Parcelize
+ data class SshKey(
+ override val id: String,
+ override val name: Text,
+ override val startIcon: IconData = IconData.Local(R.drawable.ic_ssh_key),
+ override val startIconTestTag: String = "SshKeyCipherIcon",
+ override val extraIconList: List = emptyList(),
+ override val overflowOptions: List,
+ override val shouldShowMasterPasswordReprompt: Boolean,
+ val publicKey: Text?,
+ val privateKey: Text?,
+ val fingerprint: Text?,
+ ) : VaultItem() {
+ override val supportingLabel: Text? get() = null
+ }
}
}
@@ -1154,6 +1211,11 @@ sealed class VaultAction {
*/
data object SecureNoteGroupClick : VaultAction()
+ /**
+ * User clicked the SSH key types button.
+ */
+ data object SshKeyGroupClick : VaultAction()
+
/**
* User clicked the trash button.
*/
@@ -1232,6 +1294,7 @@ sealed class VaultAction {
*/
data class VaultDataReceive(
val vaultData: DataState,
+ val showSshKeys: Boolean,
) : Internal()
/**
@@ -1272,6 +1335,7 @@ private fun MutableStateFlow.updateToErrorStateOrDialog(
hasMasterPassword = hasMasterPassword,
vaultFilterType = vaultFilterType,
isIconLoadingDisabled = isIconLoadingDisabled,
+ showSshKeys = it.showSshKeys,
),
dialog = VaultState.DialogState.Error(
title = errorTitle,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt
index 3e1106d62..0c257640c 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt
@@ -29,6 +29,7 @@ data class VaultHandlers(
val cardGroupClick: () -> Unit,
val identityGroupClick: () -> Unit,
val secureNoteGroupClick: () -> Unit,
+ val sshKeyGroupClick: () -> Unit,
val trashClick: () -> Unit,
val tryAgainClick: () -> Unit,
val dialogDismiss: () -> Unit,
@@ -77,6 +78,7 @@ data class VaultHandlers(
secureNoteGroupClick = {
viewModel.trySendAction(VaultAction.SecureNoteGroupClick)
},
+ sshKeyGroupClick = { viewModel.trySendAction(VaultAction.SshKeyGroupClick) },
trashClick = { viewModel.trySendAction(VaultAction.TrashClick) },
tryAgainClick = { viewModel.trySendAction(VaultAction.TryAgainClick) },
dialogDismiss = { viewModel.trySendAction(VaultAction.DialogDismiss) },
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt
index 4c4d64297..78b4ea852 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt
@@ -15,6 +15,7 @@ import com.bitwarden.vault.LoginView
import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SecureNoteView
+import com.bitwarden.vault.SshKeyView
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
@@ -49,6 +50,7 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
secureNote = type.toSecureNotesView(),
login = type.toLoginView(common = common),
card = type.toCardView(),
+ sshKey = type.toSshKeyView(),
// Fields we always grab from the UI
name = common.name,
@@ -66,6 +68,16 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCipherType(): CipherT
is VaultAddEditState.ViewState.Content.ItemType.Identity -> CipherType.IDENTITY
is VaultAddEditState.ViewState.Content.ItemType.Login -> CipherType.LOGIN
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> CipherType.SECURE_NOTE
+ is VaultAddEditState.ViewState.Content.ItemType.SshKey -> CipherType.SSH_KEY
+ }
+
+private fun VaultAddEditState.ViewState.Content.ItemType.toSshKeyView(): SshKeyView? =
+ (this as? VaultAddEditState.ViewState.Content.ItemType.SshKey)?.let {
+ SshKeyView(
+ publicKey = it.publicKey.orNullIfBlank(),
+ privateKey = it.privateKey.orNullIfBlank(),
+ fingerprint = it.fingerprint.orNullIfBlank(),
+ )
}
private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView? =
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt
index 6b087bd18..b4acd8860 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt
@@ -32,13 +32,14 @@ private const val NO_FOLDER_ITEM_THRESHOLD: Int = 100
/**
* Transforms [VaultData] into [VaultState.ViewState] using the given [vaultFilterType].
*/
-@Suppress("LongMethod")
+@Suppress("LongMethod", "LongParameterList")
fun VaultData.toViewState(
isPremium: Boolean,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
vaultFilterType: VaultFilterType,
+ showSshKeys: Boolean,
): VaultState.ViewState {
val filteredCipherViewListWithDeletedItems =
@@ -46,6 +47,7 @@ fun VaultData.toViewState(
val filteredCipherViewList = filteredCipherViewListWithDeletedItems
.filter { it.deletedDate == null }
+ .filterSshKeysIfNecessary(showSshKeys)
val filteredFolderViewList = folderViewList
.toFilteredList(
@@ -61,11 +63,19 @@ fun VaultData.toViewState(
val noFolderItems = filteredCipherViewList
.filter { it.folderId.isNullOrBlank() }
+ val itemTypesCount: Int = if (showSshKeys) {
+ CipherType.entries
+ } else {
+ CipherType.entries.filterNot { it == CipherType.SSH_KEY }
+ }
+ .size
+
return if (filteredCipherViewListWithDeletedItems.isEmpty()) {
VaultState.ViewState.NoItems
} else {
val totpItems = filteredCipherViewList.filter { it.login?.totp != null }
VaultState.ViewState.Content(
+ itemTypesCount = itemTypesCount,
totpItemsCount = if (isPremium) {
totpItems.count()
} else {
@@ -76,6 +86,7 @@ fun VaultData.toViewState(
identityItemsCount = filteredCipherViewList.count { it.type == CipherType.IDENTITY },
secureNoteItemsCount = filteredCipherViewList
.count { it.type == CipherType.SECURE_NOTE },
+ sshKeyItemsCount = filteredCipherViewList.count { it.type == CipherType.SSH_KEY },
favoriteItems = filteredCipherViewList
.filter { it.favorite }
.mapNotNull {
@@ -259,6 +270,26 @@ private fun CipherView.toVaultItemOrNull(
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
+
+ CipherType.SSH_KEY -> VaultState.ViewState.VaultItem.SshKey(
+ id = id,
+ name = name.asText(),
+ publicKey = sshKey
+ ?.publicKey
+ ?.asText(),
+ privateKey = sshKey
+ ?.privateKey
+ ?.asText(),
+ fingerprint = sshKey
+ ?.fingerprint
+ ?.asText(),
+ overflowOptions = toOverflowActions(
+ hasMasterPassword = hasMasterPassword,
+ isPremiumUser = isPremiumUser,
+ ),
+ extraIconList = toLabelIcons(),
+ shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
+ )
}
}
@@ -321,3 +352,16 @@ fun List.toFilteredList(
}
}
}
+
+/**
+ * Filters out all [CipherView]s that are of type [CipherType.SSH_KEY] if [showSshKeys] is false.
+ *
+ * @param showSshKeys Whether to show SSH keys in the vault.
+ */
+@JvmName("filterSshKeysIfNecessary")
+fun List.filterSshKeysIfNecessary(showSshKeys: Boolean): List =
+ if (showSshKeys) {
+ this
+ } else {
+ filter { it.type != CipherType.SSH_KEY }
+ }
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt
index a44c95207..6571a4601 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt
@@ -24,4 +24,9 @@ enum class VaultItemCipherType {
* A secure note cipher.
*/
SECURE_NOTE,
+
+ /**
+ * A SSH key cipher.
+ */
+ SSH_KEY,
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemListingType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemListingType.kt
index dc8b56760..2a49826be 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemListingType.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemListingType.kt
@@ -21,10 +21,15 @@ sealed class VaultItemListingType {
data object SecureNote : VaultItemListingType()
/**
- * A Card listing.
+ * A Card listing.
*/
data object Card : VaultItemListingType()
+ /**
+ * A SSH key listing.
+ */
+ data object SshKey : VaultItemListingType()
+
/**
* A Trash listing.
*/
diff --git a/app/src/main/res/drawable/ic_ssh_key.xml b/app/src/main/res/drawable/ic_ssh_key.xml
new file mode 100644
index 000000000..227926e9b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ssh_key.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 963b2c569..4c1c14a0f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1075,4 +1075,8 @@ Do you want to switch to this account?
No logins were imported
Logins imported
Remember to delete your imported password file from your computer
+ SSH key
+ Public key
+ Private key
+ SSH keys
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/util/CipherViewExtensionsTest.kt
index 66474e3ec..8db5de845 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/util/CipherViewExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/util/CipherViewExtensionsTest.kt
@@ -310,6 +310,18 @@ class CipherViewExtensionsTest {
assertEquals(expected, actual)
}
+ @Test
+ fun `subtitle should return null when type is IDENTITY and identity is null`() {
+ val cipherView: CipherView = mockk {
+ every { identity } returns null
+ every { type } returns CipherType.IDENTITY
+ }
+
+ val actual = cipherView.subtitle
+
+ assertNull(actual)
+ }
+
@Suppress("MaxLineLength")
@Test
fun `subtitle should return null when type is IDENTITY and first and last name are null`() {
@@ -329,4 +341,15 @@ class CipherViewExtensionsTest {
// Verify
assertNull(actual)
}
+
+ @Test
+ fun `subtitle should return null when type is SSH_KEY`() {
+ val cipherView: CipherView = mockk {
+ every { type } returns CipherType.SSH_KEY
+ }
+
+ val actual = cipherView.subtitle
+
+ assertNull(actual)
+ }
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt
index 7613fe173..085611f39 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt
@@ -407,7 +407,12 @@ private const val CIPHER_JSON = """
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
},
- "key": "mockKey-1"
+ "key": "mockKey-1",
+ "sshKey": {
+ "publicKey": "mockPublicKey-1",
+ "privateKey": "mockPrivateKey-1",
+ "keyFingerprint": "mockKeyFingerprint-1"
+ }
}
"""
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequestUtil.kt
index fbcaef7cf..dcff50d18 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequestUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CipherJsonRequestUtil.kt
@@ -21,6 +21,7 @@ fun createMockCipherJsonRequest(number: Int, hasNullUri: Boolean = false): Ciphe
type = CipherTypeJson.LOGIN,
login = createMockLogin(number = number, hasNullUri = hasNullUri),
card = createMockCard(number = number),
+ sshKey = createMockSshKey(number = number),
fields = listOf(createMockField(number = number)),
identity = createMockIdentity(number = number),
isFavorite = false,
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt
index f6be71879..1ac03e59e 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt
@@ -38,6 +38,7 @@ fun createMockCipher(
card = createMockCard(number = number),
fields = listOf(createMockField(number = number)),
identity = createMockIdentity(number = number),
+ sshKey = createMockSshKey(number = number),
isFavorite = false,
passwordHistory = listOf(createMockPasswordHistory(number = number)),
reprompt = CipherRepromptTypeJson.NONE,
@@ -148,6 +149,18 @@ fun createMockLogin(
fido2Credentials = fido2Credentials,
)
+/**
+ * Create a mock [SyncResponseJson.Cipher.SshKey] with a given [number].
+ */
+fun createMockSshKey(number: Int) = SyncResponseJson.Cipher.SshKey(
+ publicKey = "mockPublicKey-$number",
+ privateKey = "mockPrivateKey-$number",
+ keyFingerprint = "mockKeyFingerprint-$number",
+)
+
+/**
+ * Create a mock [SyncResponseJson.Cipher.Fido2Credential] with a given [number].
+ */
fun createMockFido2Credential(number: Int) = SyncResponseJson.Cipher.Fido2Credential(
credentialId = "mockCredentialId-$number",
keyType = "mockKeyType-$number",
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt
index e8134e016..50c6b07a4 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt
@@ -465,7 +465,12 @@ private const val CREATE_ATTACHMENT_SUCCESS_JSON = """
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
},
- "key": "mockKey-1"
+ "key": "mockKey-1",
+ "sshKey": {
+ "publicKey": "mockPublicKey-1",
+ "privateKey": "mockPrivateKey-1",
+ "keyFingerprint": "mockKeyFingerprint-1"
+ }
}
}
"""
@@ -576,7 +581,12 @@ private const val CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON = """
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
},
- "key": "mockKey-1"
+ "key": "mockKey-1",
+ "sshKey": {
+ "publicKey": "mockPublicKey-1",
+ "privateKey": "mockPrivateKey-1",
+ "keyFingerprint": "mockKeyFingerprint-1"
+ }
}
"""
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt
index f2b4a9e4f..8ca29fa96 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt
@@ -302,7 +302,12 @@ private const val SYNC_SUCCESS_JSON = """
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
},
- "key": "mockKey-1"
+ "key": "mockKey-1",
+ "sshKey": {
+ "publicKey": "mockPublicKey-1",
+ "privateKey": "mockPrivateKey-1",
+ "keyFingerprint": "mockKeyFingerprint-1"
+ }
}
],
"domains": {
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt
index 936e98fcd..3498970ac 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt
@@ -15,6 +15,7 @@ import com.bitwarden.vault.LoginView
import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SecureNoteView
+import com.bitwarden.vault.SshKeyView
import com.bitwarden.vault.UriMatchType
import java.time.Clock
import java.time.Instant
@@ -47,6 +48,7 @@ fun createMockCipherView(
folderId: String? = "mockId-$number",
clock: Clock = FIXED_CLOCK,
fido2Credentials: List? = null,
+ sshKey: SshKeyView? = createMockSshKeyView(number = number),
): CipherView =
CipherView(
id = "mockId-$number",
@@ -77,6 +79,7 @@ fun createMockCipherView(
identity = createMockIdentityView(number = number).takeIf {
cipherType == CipherType.IDENTITY
},
+ sshKey = sshKey.takeIf { cipherType == CipherType.SSH_KEY },
favorite = false,
passwordHistory = listOf(createMockPasswordHistoryView(number = number, clock)),
reprompt = repromptType,
@@ -223,6 +226,16 @@ fun createMockIdentityView(number: Int): IdentityView =
username = "mockUsername-$number",
)
+/**
+ * Create a mock [SshKeyView] with a given [number].
+ */
+fun createMockSshKeyView(number: Int): SshKeyView =
+ SshKeyView(
+ publicKey = "mockPublicKey-$number",
+ privateKey = "mockPrivateKey-$number",
+ fingerprint = "mockKeyFingerprint-$number",
+ )
+
/**
* Create a mock [PasswordHistoryView] with a given [number].
*/
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt
index ed3ce0cce..2920e071f 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt
@@ -13,6 +13,7 @@ import com.bitwarden.vault.LoginUri
import com.bitwarden.vault.PasswordHistory
import com.bitwarden.vault.SecureNote
import com.bitwarden.vault.SecureNoteType
+import com.bitwarden.vault.SshKey
import com.bitwarden.vault.UriMatchType
import java.time.Clock
import java.time.Instant
@@ -49,6 +50,7 @@ fun createMockSdkCipher(number: Int, clock: Clock = FIXED_CLOCK): Cipher =
card = createMockSdkCard(number = number),
fields = listOf(createMockSdkField(number = number)),
identity = createMockSdkIdentity(number = number),
+ sshKey = createMockSdkSshKey(number = number),
favorite = false,
passwordHistory = listOf(createMockSdkPasswordHistory(number = number, clock = clock)),
reprompt = CipherRepromptType.NONE,
@@ -101,6 +103,16 @@ fun createMockSdkIdentity(number: Int): Identity =
username = "mockUsername-$number",
)
+/**
+ * Create a mock [SshKey] with a given [number].
+ */
+fun createMockSdkSshKey(number: Int): SshKey =
+ SshKey(
+ publicKey = "mockPublicKey-$number",
+ privateKey = "mockPrivateKey-$number",
+ fingerprint = "mockKeyFingerprint-$number",
+ )
+
/**
* Create a mock [Field] with a given [number].
*/
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt
index d8c902995..702afa8cc 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt
@@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockIdentit
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockLogin
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPasswordHistory
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSecureNote
+import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSshKey
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockUri
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment
@@ -28,6 +29,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkIdentity
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkLogin
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkPasswordHistory
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSecureNote
+import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSshKey
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkUri
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -152,6 +154,16 @@ class VaultSdkCipherExtensionsTest {
)
}
+ @Test
+ fun `toSdkSshKey should convert a SyncResponseJson Cipher SshKey to a SshKey`() {
+ val syncSshKey = createMockSshKey(number = 1)
+ val sdkSshKey = syncSshKey.toSdkSshKey()
+ assertEquals(
+ createMockSdkSshKey(number = 1),
+ sdkSshKey,
+ )
+ }
+
@Test
fun `toSdkLoginUriList should convert list of LoginUri to List of Sdk LoginUri`() {
val syncLoginUris = listOf(
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt
index 91fbc0164..734af84e6 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt
@@ -485,6 +485,31 @@ class SearchScreenTest : BaseComposeTest() {
}
composeTestRule.onNodeWithText(text = "Search Verification codes").assertIsDisplayed()
+ mutableStateFlow.update {
+ it.copy(searchType = SearchTypeData.Vault.SshKeys)
+ }
+ composeTestRule.onNodeWithText(text = "Search SSH keys").assertIsDisplayed()
+
+ mutableStateFlow.update {
+ it.copy(searchType = SearchTypeData.Vault.Logins)
+ }
+ composeTestRule.onNodeWithText(text = "Search Logins").assertIsDisplayed()
+
+ mutableStateFlow.update {
+ it.copy(searchType = SearchTypeData.Vault.NoFolder)
+ }
+ composeTestRule.onNodeWithText(text = "Search No Folder").assertIsDisplayed()
+
+ mutableStateFlow.update {
+ it.copy(
+ searchType = SearchTypeData.Vault.Collection(
+ collectionId = "mockId",
+ collectionName = "mockName",
+ ),
+ )
+ }
+ composeTestRule.onNodeWithText(text = "Search mockName").assertIsDisplayed()
+
mutableStateFlow.update {
it.copy(
searchType = SearchTypeData.Vault.Folder(
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt
index 9936ac3e8..dd528c56f 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt
@@ -1444,6 +1444,7 @@ class SearchViewModelTest : BaseViewModelTest() {
SearchTypeData.Sends.Texts -> "search_type_sends_text"
SearchTypeData.Vault.All -> "search_type_vault_all"
SearchTypeData.Vault.Cards -> "search_type_vault_cards"
+ SearchTypeData.Vault.SshKeys -> "search_type_vault_ssh_keys"
is SearchTypeData.Vault.Collection -> "search_type_vault_collection"
is SearchTypeData.Vault.Folder -> "search_type_vault_folder"
SearchTypeData.Vault.Identities -> "search_type_vault_identities"
@@ -1463,6 +1464,7 @@ class SearchViewModelTest : BaseViewModelTest() {
SearchTypeData.Sends.Texts -> null
SearchTypeData.Vault.All -> null
SearchTypeData.Vault.Cards -> null
+ SearchTypeData.Vault.SshKeys -> null
is SearchTypeData.Vault.Collection -> searchType.collectionId
is SearchTypeData.Vault.Folder -> searchType.folderId
SearchTypeData.Vault.Identities -> null
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensionsTest.kt
index 7898c1698..ccecb1bcb 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensionsTest.kt
@@ -203,6 +203,32 @@ class SearchTypeDataExtensionsTest {
)
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `updateWithAdditionalDataIfNecessary should return the searchTypeData unchanged for Vault SshKeys`() {
+ val searchTypeData = SearchTypeData.Vault.SshKeys
+ assertEquals(
+ searchTypeData,
+ searchTypeData.updateWithAdditionalDataIfNecessary(
+ folderList = listOf(),
+ collectionList = emptyList(),
+ ),
+ )
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `updateWithAdditionalDataIfNecessary should return the searchTypeData unchanged for Vault VerificationCodes`() {
+ val searchTypeData = SearchTypeData.Vault.VerificationCodes
+ assertEquals(
+ searchTypeData,
+ searchTypeData.updateWithAdditionalDataIfNecessary(
+ folderList = listOf(),
+ collectionList = emptyList(),
+ ),
+ )
+ }
+
@Suppress("MaxLineLength")
@Test
fun `updateWithAdditionalDataIfNecessary should return the searchTypeData unchanged for Vault Trash`() {
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensionsTest.kt
index 7077fe0ee..d7757078b 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeExtensionsTest.kt
@@ -80,4 +80,18 @@ class SearchTypeExtensionsTest {
fun `toSearchTypeData should return Vault Trash then SearchType is Vault Trash`() {
assertEquals(SearchTypeData.Vault.Trash, SearchType.Vault.Trash.toSearchTypeData())
}
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `toSearchTypeData should return Vault VerificationCodes then SearchType is Vault VerificationCodes`() {
+ assertEquals(
+ SearchTypeData.Vault.VerificationCodes,
+ SearchType.Vault.VerificationCodes.toSearchTypeData(),
+ )
+ }
+
+ @Test
+ fun `toSearchTypeData should return Vault SshKeys then SearchType is Vault SshKeys`() {
+ assertEquals(SearchTypeData.Vault.SshKeys, SearchType.Vault.SshKeys.toSearchTypeData())
+ }
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt
index 80736f1bf..a3e23fe2b 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt
@@ -186,6 +186,36 @@ fun createMockDisplayItemForCipher(
isTotp = false,
)
}
+
+ CipherType.SSH_KEY -> {
+ SearchState.DisplayItem(
+ id = "mockId-$number",
+ title = "mockName-$number",
+ titleTestTag = "CipherNameLabel",
+ subtitle = "mockPublicKey-$number",
+ subtitleTestTag = "CipherSubTitleLabel",
+ iconData = IconData.Local(R.drawable.ic_ssh_key),
+ extraIconList = listOf(
+ IconRes(
+ iconRes = R.drawable.ic_collections,
+ contentDescription = R.string.collections.asText(),
+ testTag = "CipherInCollectionIcon",
+ ),
+ ),
+ overflowOptions = listOf(
+ ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
+ ListingItemOverflowAction.VaultAction.EditClick(
+ cipherId = "mockId-$number",
+ requiresPasswordReprompt = true,
+ ),
+ ),
+ overflowTestTag = "CipherOptionsButton",
+ totpCode = null,
+ autofillSelectionOptions = emptyList(),
+ shouldDisplayMasterPasswordReprompt = false,
+ isTotp = false,
+ )
+ }
}
/**
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt
index db34dd628..1956f980a 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt
@@ -3322,6 +3322,77 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
}
+ @Test
+ fun `in ItemType_SshKeys changing the public key should trigger PublicKeyTextChange`() {
+ mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
+
+ composeTestRule
+ .onNodeWithTextAfterScroll("Public key")
+ .performTextInput("TestPublicKey")
+
+ verify {
+ viewModel.trySendAction(
+ VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
+ publicKey = "TestPublicKey",
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `in ItemType_SshKeys changing the private key should trigger PrivateKeyTextChange`() {
+ mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
+
+ composeTestRule
+ .onNodeWithTextAfterScroll("Private key")
+ .performTextInput("TestPrivateKey")
+
+ verify {
+ viewModel.trySendAction(
+ VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
+ privateKey = "TestPrivateKey",
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `in ItemType_SshKeys changing the fingerprint should trigger FingerprintTextChange`() {
+ mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
+
+ composeTestRule
+ .onNodeWithTextAfterScroll("Fingerprint")
+ .performTextInput("TestFingerprint")
+
+ verify {
+ viewModel.trySendAction(
+ VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
+ fingerprint = "TestFingerprint",
+ ),
+ )
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `in ItemType_SshKeys changing the private key visibility should trigger PrivateKeyVisibilityChange`() {
+ mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
+
+ composeTestRule
+ .onNodeWithTextAfterScroll(text = "Private key")
+ .assertExists()
+ composeTestRule
+ .onNodeWithContentDescriptionAfterScroll(label = "Show")
+ .assertExists()
+ .performClick()
+
+ verify(exactly = 1) {
+ viewModel.trySendAction(
+ VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange(true),
+ )
+ }
+ }
+
//region Helper functions
private fun updateLoginType(
@@ -3444,6 +3515,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
),
dialog = VaultAddEditState.DialogState.Generic(message = "test".asText()),
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
)
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
@@ -3454,6 +3526,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
isIndividualVaultDisabled = false,
),
dialog = null,
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
)
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
@@ -3464,6 +3537,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
isIndividualVaultDisabled = false,
),
dialog = null,
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
)
private val DEFAULT_STATE_CARD = VaultAddEditState(
@@ -3474,6 +3548,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
isIndividualVaultDisabled = false,
),
dialog = null,
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
)
@Suppress("MaxLineLength")
@@ -3495,6 +3570,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
),
dialog = null,
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SECURE_NOTE),
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
)
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
@@ -3505,6 +3581,18 @@ class VaultAddEditScreenTest : BaseComposeTest() {
isIndividualVaultDisabled = false,
),
dialog = null,
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
+ )
+
+ private val DEFAULT_STATE_SSH_KEYS = VaultAddEditState(
+ vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SSH_KEY),
+ viewState = VaultAddEditState.ViewState.Content(
+ common = VaultAddEditState.ViewState.Content.Common(),
+ type = VaultAddEditState.ViewState.Content.ItemType.SshKey(),
+ isIndividualVaultDisabled = false,
+ ),
+ dialog = null,
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
)
private val ALTERED_COLLECTIONS = listOf(
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt
index 5169d97d9..4bd49bbd4 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt
@@ -25,12 +25,14 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRe
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@@ -152,6 +154,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
private val organizationEventManager = mockk {
every { trackEvent(event = any()) } just runs
}
+ private val mutableSshVaultItemsFeatureFlagFlow = MutableStateFlow(true)
+ private val featureFlagManager = mockk {
+ every {
+ getFeatureFlagFlow(key = FlagKey.SshKeyCipherItems)
+ } returns mutableSshVaultItemsFeatureFlagFlow
+ every {
+ getFeatureFlag(key = FlagKey.SshKeyCipherItems)
+ } returns mutableSshVaultItemsFeatureFlagFlow.value
+ }
@BeforeEach
fun setup() {
@@ -250,6 +261,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
type = VaultAddEditState.ViewState.Content.ItemType.Login(),
),
dialog = null,
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
),
viewModel.stateFlow.value,
)
@@ -365,6 +377,56 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
}
+ @Test
+ fun `initial add state should be correct when SSH key feature flag is enabled`() {
+ mutableSshVaultItemsFeatureFlagFlow.value = true
+ val vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN)
+ val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType)
+ val viewModel = createAddVaultItemViewModel(
+ savedStateHandle = createSavedStateHandleWithState(
+ state = initState,
+ vaultAddEditType = vaultAddEditType,
+ ),
+ )
+ assertEquals(
+ initState,
+ viewModel.stateFlow.value,
+ )
+ }
+
+ @Test
+ fun `initial add state should be correct when SSH key feature flag is disabled`() {
+ mutableSshVaultItemsFeatureFlagFlow.value = false
+ every {
+ featureFlagManager.getFeatureFlag(key = FlagKey.SshKeyCipherItems)
+ } returns false
+ val vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN)
+ val expectedState = VaultAddEditState(
+ vaultAddEditType = vaultAddEditType,
+ viewState = VaultAddEditState.ViewState.Content(
+ common = VaultAddEditState.ViewState.Content.Common(),
+ isIndividualVaultDisabled = false,
+ type = VaultAddEditState.ViewState.Content.ItemType.Login(),
+ ),
+ dialog = null,
+ totpData = null,
+ shouldShowCloseButton = true,
+ shouldExitOnSave = false,
+ supportedItemTypes = VaultAddEditState.ItemTypeOption.entries
+ .filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS },
+ )
+ val viewModel = createAddVaultItemViewModel(
+ savedStateHandle = createSavedStateHandleWithState(
+ state = null,
+ vaultAddEditType = vaultAddEditType,
+ ),
+ )
+ assertEquals(
+ expectedState,
+ viewModel.stateFlow.value,
+ )
+ }
+
@Test
fun `initial edit state should be correct`() = runTest {
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
@@ -1792,6 +1854,36 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
}
+ @Test
+ fun `TypeOptionSelect SSH_KEYS should switch to SshKeysItem`() = runTest {
+ mutableVaultDataFlow.value = DataState.Loaded(
+ createVaultData(cipherView = createMockCipherView(1)),
+ )
+ val viewModel = createAddVaultItemViewModel()
+ val action = VaultAddEditAction.Common.TypeOptionSelect(
+ VaultAddEditState.ItemTypeOption.SSH_KEYS,
+ )
+
+ viewModel.trySendAction(action)
+
+ val expectedState = loginInitialState.copy(
+ viewState = VaultAddEditState.ViewState.Content(
+ common = createCommonContentViewState(),
+ isIndividualVaultDisabled = false,
+ type = VaultAddEditState.ViewState.Content.ItemType.SshKey(),
+ previousItemTypes = mapOf(
+ VaultAddEditState.ItemTypeOption.LOGIN
+ to VaultAddEditState.ViewState.Content.ItemType.Login(),
+ ),
+ ),
+ )
+
+ assertEquals(
+ expectedState,
+ viewModel.stateFlow.value,
+ )
+ }
+
@Nested
inner class VaultAddEditLoginTypeItemActions {
private lateinit var viewModel: VaultAddEditViewModel
@@ -2610,6 +2702,90 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
}
+ @Nested
+ inner class VaultAddEditSshKeyTypeItemActions {
+ private lateinit var viewModel: VaultAddEditViewModel
+ private lateinit var vaultAddItemInitialState: VaultAddEditState
+ private lateinit var sshKeyInitialSavedStateHandle: SavedStateHandle
+
+ @BeforeEach
+ fun setup() {
+ mutableVaultDataFlow.value = DataState.Loaded(
+ createVaultData(cipherView = createMockCipherView(1)),
+ )
+ vaultAddItemInitialState = createVaultAddItemState(
+ typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(),
+ )
+ sshKeyInitialSavedStateHandle = createSavedStateHandleWithState(
+ state = vaultAddItemInitialState,
+ vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SSH_KEY),
+ )
+ viewModel = createAddVaultItemViewModel(
+ savedStateHandle = sshKeyInitialSavedStateHandle,
+ )
+ }
+
+ @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(
+ isVisible = true,
+ )
+ val expectedState = createVaultAddItemState(
+ typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
+ showPrivateKey = true,
+ ),
+ )
+ viewModel.trySendAction(action)
+
+ 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
fun `NumberVisibilityChange should log an event when in edit mode and password is visible`() =
runTest {
@@ -2686,6 +2862,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
organizationEventManager = organizationEventManager,
+ featureFlagManager = featureFlagManager,
)
}
@@ -3781,6 +3958,30 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
}
}
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `SshKeyCipherItemsFeatureFlagReceive should update supportedItemTypes`() = runTest {
+ // Verify SSH keys is supported when feature flag is enabled.
+ viewModel.trySendAction(
+ VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(enabled = true),
+ )
+ assertEquals(
+ VaultAddEditState.ItemTypeOption.entries,
+ viewModel.stateFlow.value.supportedItemTypes,
+ )
+
+ // Verify SSH keys is not supported when feature flag is disabled.
+ viewModel.trySendAction(
+ VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(enabled = false),
+ )
+ assertEquals(
+ VaultAddEditState.ItemTypeOption.entries.filterNot {
+ it == VaultAddEditState.ItemTypeOption.SSH_KEYS
+ },
+ viewModel.stateFlow.value.supportedItemTypes,
+ )
+ }
}
//region Helper functions
@@ -3794,6 +3995,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
typeContentViewState: VaultAddEditState.ViewState.Content.ItemType = createLoginTypeContentViewState(),
dialogState: VaultAddEditState.DialogState? = null,
totpData: TotpData? = null,
+ supportedItemTypes: List = VaultAddEditState.ItemTypeOption.entries,
): VaultAddEditState =
VaultAddEditState(
vaultAddEditType = vaultAddEditType,
@@ -3805,6 +4007,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
dialog = dialogState,
shouldExitOnSave = shouldExitOnSave,
totpData = totpData,
+ supportedItemTypes = supportedItemTypes,
)
@Suppress("LongParameterList")
@@ -3879,6 +4082,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
VaultItemCipherType.CARD -> "card"
VaultItemCipherType.IDENTITY -> "identity"
VaultItemCipherType.SECURE_NOTE -> "secure_note"
+ VaultItemCipherType.SSH_KEY -> "ssh_key"
null -> null
},
)
@@ -3906,6 +4110,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = bitwardenResourceManager,
clock = clock,
organizationEventManager = organizationEventManager,
+ featureFlagManager = featureFlagManager,
)
private fun createVaultData(
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt
index 58a6cf633..02c551f71 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt
@@ -12,6 +12,7 @@ import com.bitwarden.vault.LoginView
import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SecureNoteView
+import com.bitwarden.vault.SshKeyView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.model.Organization
@@ -313,6 +314,50 @@ class CipherViewExtensionsTest {
)
}
+ @Test
+ fun `toViewState should create SSH Key ViewState`() {
+ val cipherView = DEFAULT_SSH_KEY_CIPHER_VIEW
+
+ val result = cipherView.toViewState(
+ isClone = false,
+ isIndividualVaultDisabled = false,
+ totpData = null,
+ resourceManager = resourceManager,
+ clock = FIXED_CLOCK,
+ )
+
+ assertEquals(
+ VaultAddEditState.ViewState.Content(
+ common = VaultAddEditState.ViewState.Content.Common(
+ originalCipher = cipherView,
+ name = "cipher",
+ favorite = false,
+ masterPasswordReprompt = true,
+ notes = "Lots of notes",
+ customFieldData = listOf(
+ VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false),
+ VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"),
+ VaultAddEditState.Custom.HiddenField(TEST_ID, "TestHidden", "TestHidden"),
+ VaultAddEditState.Custom.LinkedField(
+ TEST_ID,
+ "TestLinked",
+ VaultLinkedFieldType.USERNAME,
+ ),
+ ),
+ availableFolders = emptyList(),
+ availableOwners = emptyList(),
+ ),
+ isIndividualVaultDisabled = false,
+ type = VaultAddEditState.ViewState.Content.ItemType.SshKey(
+ publicKey = "PublicKey",
+ privateKey = "PrivateKey",
+ fingerprint = "Fingerprint",
+ ),
+ ),
+ result,
+ )
+ }
+
@Test
fun `toViewState with isClone true should append clone text to the cipher name`() {
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
@@ -578,6 +623,7 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = FIXED_CLOCK.instant(),
+ sshKey = null,
)
private val DEFAULT_CARD_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
@@ -660,4 +706,13 @@ private val DEFAULT_SECURE_NOTES_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_V
secureNote = SecureNoteView(type = SecureNoteType.GENERIC),
)
+private val DEFAULT_SSH_KEY_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
+ type = CipherType.SSH_KEY,
+ sshKey = SshKeyView(
+ publicKey = "PublicKey",
+ privateKey = "PrivateKey",
+ fingerprint = "Fingerprint",
+ ),
+)
+
private const val TEST_ID = "testID"
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensionsTest.kt
index aa4d49997..483d37337 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/VaultAddEditExtensionsTest.kt
@@ -31,6 +31,7 @@ class VaultAddEditExtensionsTest {
VaultItemCipherType.CARD,
VaultItemCipherType.SECURE_NOTE,
VaultItemCipherType.IDENTITY,
+ VaultItemCipherType.SSH_KEY,
)
val result = vaultItemCipherTypeList.map { it.toItemType() }
@@ -41,6 +42,7 @@ class VaultAddEditExtensionsTest {
VaultAddEditState.ViewState.Content.ItemType.Card(),
VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
VaultAddEditState.ViewState.Content.ItemType.Identity(),
+ VaultAddEditState.ViewState.Content.ItemType.SshKey(),
),
result,
)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt
index 803811bd2..22d67c1d4 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt
@@ -1867,6 +1867,8 @@ class VaultItemScreenTest : BaseComposeTest() {
}
//endregion identity
+ //region card
+
@Test
fun `in card state, cardholderName should be displayed according to state`() {
val cardholderName = "the cardholder name"
@@ -2139,6 +2141,79 @@ class VaultItemScreenTest : BaseComposeTest() {
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
}
}
+
+ //endregion card
+
+ //region ssh key
+
+ @Test
+ fun `in ssh key state, public key should be displayed according to state`() {
+ val publicKey = "the public key"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(publicKey).assertIsDisplayed()
+
+ mutableStateFlow.update { currentState ->
+ updateSshKeyType(currentState) { copy(publicKey = null) }
+ }
+
+ composeTestRule.assertScrollableNodeDoesNotExist(publicKey)
+ }
+
+ @Test
+ fun `in ssh key state, private key should be displayed according to state`() {
+ val privateKey = "the private key"
+ mutableStateFlow.update {
+ it.copy(
+ viewState = DEFAULT_SSH_KEY_VIEW_STATE
+ .copy(
+ type = DEFAULT_SSH_KEY.copy(showPrivateKey = true),
+ ),
+ )
+ }
+ composeTestRule
+ .onNodeWithText(privateKey)
+ .assertIsDisplayed()
+
+ mutableStateFlow.update { currentState ->
+ updateSshKeyType(currentState) { copy(privateKey = null) }
+ }
+
+ composeTestRule.assertScrollableNodeDoesNotExist(privateKey)
+ }
+
+ @Test
+ fun `in ssh key state, on show private key click should send ShowPrivateKeyClick`() {
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
+
+ composeTestRule
+ .onNodeWithTextAfterScroll("Private key")
+ .onChildren()
+ .filterToOne(hasContentDescription("Show"))
+ .performClick()
+
+ verify(exactly = 1) {
+ viewModel.trySendAction(
+ VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
+ isVisible = true,
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `in ssh key state, fingerprint should be displayed according to state`() {
+ val fingerprint = "the fingerprint"
+ mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
+ composeTestRule.onNodeWithTextAfterScroll(fingerprint).assertIsDisplayed()
+
+ mutableStateFlow.update { currentState ->
+ updateSshKeyType(currentState) { copy(fingerprint = null) }
+ }
+
+ composeTestRule.assertScrollableNodeDoesNotExist(fingerprint)
+ }
+
+ //endregion ssh key
}
//region Helper functions
@@ -2212,6 +2287,29 @@ private fun updateCardType(
return currentState.copy(viewState = updatedType)
}
+private fun updateSshKeyType(
+ currentState: VaultItemState,
+ transform: VaultItemState.ViewState.Content.ItemType.SshKey.() ->
+ VaultItemState.ViewState.Content.ItemType.SshKey,
+): VaultItemState {
+ val updatedType = when (val viewState = currentState.viewState) {
+ is VaultItemState.ViewState.Content -> {
+ when (val type = viewState.type) {
+ is VaultItemState.ViewState.Content.ItemType.SshKey -> {
+ viewState.copy(
+ type = type.transform(),
+ )
+ }
+
+ else -> viewState
+ }
+ }
+
+ else -> viewState
+ }
+ return currentState.copy(viewState = updatedType)
+}
+
private fun updateCommonContent(
currentState: VaultItemState,
transform: VaultItemState.ViewState.Content.Common.()
@@ -2333,6 +2431,15 @@ private val DEFAULT_CARD: VaultItemState.ViewState.Content.ItemType.Card =
),
)
+private val DEFAULT_SSH_KEY: VaultItemState.ViewState.Content.ItemType.SshKey =
+ VaultItemState.ViewState.Content.ItemType.SshKey(
+ name = "the ssh key name",
+ publicKey = "the public key",
+ privateKey = "the private key",
+ fingerprint = "the fingerprint",
+ showPrivateKey = false,
+ )
+
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
name = "cipher",
@@ -2433,6 +2540,12 @@ private val DEFAULT_SECURE_NOTE_VIEW_STATE: VaultItemState.ViewState.Content =
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
)
+private val DEFAULT_SSH_KEY_VIEW_STATE: VaultItemState.ViewState.Content =
+ VaultItemState.ViewState.Content(
+ common = DEFAULT_COMMON,
+ type = DEFAULT_SSH_KEY,
+ )
+
private val EMPTY_VIEW_STATES = listOf(
EMPTY_LOGIN_VIEW_STATE,
EMPTY_IDENTITY_VIEW_STATE,
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt
index 160e78575..be2bc9512 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt
@@ -2336,6 +2336,60 @@ class VaultItemViewModelTest : BaseViewModelTest() {
}
}
+ @Nested
+ inner class SshKeyActions {
+ private lateinit var viewModel: VaultItemViewModel
+
+ @BeforeEach
+ fun setup() {
+ viewModel = createViewModel(
+ state = DEFAULT_STATE.copy(
+ viewState = SSH_KEY_VIEW_STATE,
+ ),
+ )
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on PrivateKeyVisibilityClick should show password dialog when re-prompt is required`() =
+ runTest {
+ val sshKeyViewState = createViewState(
+ common = DEFAULT_COMMON.copy(requiresReprompt = false),
+ )
+ val sshKeyState = DEFAULT_STATE.copy(viewState = SSH_KEY_VIEW_STATE)
+ val mockCipherView = mockk {
+ 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)
+
+ assertEquals(sshKeyState, viewModel.stateFlow.value)
+ viewModel.trySendAction(
+ VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
+ isVisible = true,
+ ),
+ )
+ assertEquals(
+ sshKeyState.copy(
+ viewState = sshKeyViewState.copy(
+ common = DEFAULT_COMMON,
+ type = DEFAULT_SSH_KEY_TYPE.copy(
+ showPrivateKey = true,
+ ),
+ ),
+ ),
+ viewModel.stateFlow.value,
+ )
+ }
+ }
+
@Nested
inner class VaultItemFlow {
@BeforeEach
@@ -2628,6 +2682,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
),
)
+ private val DEFAULT_SSH_KEY_TYPE: VaultItemState.ViewState.Content.ItemType.SshKey =
+ VaultItemState.ViewState.Content.ItemType.SshKey(
+ name = "mockName",
+ publicKey = "mockPublicKey",
+ privateKey = "mockPrivateKey",
+ fingerprint = "mockFingerprint",
+ showPrivateKey = false,
+ )
+
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
name = "login cipher",
@@ -2684,5 +2747,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
common = DEFAULT_COMMON,
type = DEFAULT_CARD_TYPE,
)
+
+ private val SSH_KEY_VIEW_STATE: VaultItemState.ViewState.Content =
+ VaultItemState.ViewState.Content(
+ common = DEFAULT_COMMON,
+ type = DEFAULT_SSH_KEY_TYPE,
+ )
}
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt
index 3ceefb197..b2ab66549 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt
@@ -374,4 +374,32 @@ class CipherViewExtensionsTest {
assertEquals(expectedState, viewState)
}
+
+ @Test
+ fun `toViewState should transform full CipherView into ViewState SSH Key Content`() {
+ val cipherView = createCipherView(type = CipherType.SSH_KEY, isEmpty = false)
+ val viewState = cipherView.toViewState(
+ previousState = null,
+ isPremiumUser = true,
+ hasMasterPassword = true,
+ totpCodeItemData = null,
+ clock = fixedClock,
+ )
+ assertEquals(
+ VaultItemState.ViewState.Content(
+ common = createCommonContent(isEmpty = false, isPremiumUser = true).copy(
+ currentCipher = cipherView.copy(
+ name = "mockName",
+ sshKey = cipherView.sshKey?.copy(
+ publicKey = "publicKey",
+ privateKey = "privateKey",
+ fingerprint = "fingerprint",
+ ),
+ ),
+ ),
+ type = createSshKeyContent(isEmpty = false),
+ ),
+ viewState,
+ )
+ }
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt
index e8dbc22e1..c07c29879 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt
@@ -10,6 +10,7 @@ import com.bitwarden.vault.IdentityView
import com.bitwarden.vault.LoginUriView
import com.bitwarden.vault.LoginView
import com.bitwarden.vault.PasswordHistoryView
+import com.bitwarden.vault.SshKeyView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
import com.x8bit.bitwarden.ui.platform.base.util.asText
@@ -72,6 +73,13 @@ fun createIdentityView(isEmpty: Boolean): IdentityView =
licenseNumber = "licenseNumber".takeUnless { isEmpty },
)
+fun createSshKeyView(isEmpty: Boolean): SshKeyView =
+ SshKeyView(
+ privateKey = "privateKey".takeUnless { isEmpty },
+ publicKey = "publicKey".takeUnless { isEmpty },
+ fingerprint = "fingerprint".takeUnless { isEmpty },
+ )
+
fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView =
CipherView(
id = null,
@@ -146,6 +154,7 @@ fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView =
creationDate = Instant.ofEpochSecond(1_000L),
deletedDate = null,
revisionDate = Instant.ofEpochSecond(1_000L),
+ sshKey = createSshKeyView(isEmpty = isEmpty),
)
fun createCommonContent(
@@ -259,3 +268,12 @@ fun createIdentityContent(
phone = "phone".takeUnless { isEmpty },
address = address.takeUnless { isEmpty },
)
+
+fun createSshKeyContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemType.SshKey =
+ VaultItemState.ViewState.Content.ItemType.SshKey(
+ name = "mockName".takeUnless { isEmpty },
+ privateKey = "privateKey".takeUnless { isEmpty },
+ publicKey = "publicKey".takeUnless { isEmpty },
+ fingerprint = "fingerprint".takeUnless { isEmpty },
+ showPrivateKey = false,
+ )
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt
index 1222a9f27..f84921b60 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt
@@ -1199,6 +1199,13 @@ class VaultItemListingScreenTest : BaseComposeTest() {
.onNodeWithText(text = "Identities")
.assertIsDisplayed()
+ mutableStateFlow.update {
+ it.copy(itemListingType = VaultItemListingState.ItemListingType.Vault.SshKey)
+ }
+ composeTestRule
+ .onNodeWithText(text = "SSH keys")
+ .assertIsDisplayed()
+
mutableStateFlow.update {
it.copy(itemListingType = VaultItemListingState.ItemListingType.Vault.Trash)
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt
index 7eeb19f82..591a0f1e4 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt
@@ -3971,6 +3971,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
is VaultItemListingType.Trash -> "trash"
is VaultItemListingType.SendFile -> "send_file"
is VaultItemListingType.SendText -> "send_text"
+ is VaultItemListingType.SshKey -> "ssh_key"
},
)
set(
@@ -3985,6 +3986,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
is VaultItemListingType.Trash -> null
is VaultItemListingType.SendFile -> null
is VaultItemListingType.SendText -> null
+ is VaultItemListingType.SshKey -> null
},
)
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt
index 891e74680..db50ecce2 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt
@@ -254,6 +254,66 @@ class VaultItemListingDataExtensionsTest {
}
}
+ @Test
+ @Suppress("MaxLineLength")
+ fun `determineListingPredicate should return the correct predicate for a trash SshKey cipherView`() {
+ val cipherView = createMockCipherView(
+ number = 1,
+ isDeleted = true,
+ cipherType = CipherType.SSH_KEY,
+ )
+
+ mapOf(
+ VaultItemListingState.ItemListingType.Vault.Login to false,
+ VaultItemListingState.ItemListingType.Vault.Card to false,
+ VaultItemListingState.ItemListingType.Vault.SecureNote to false,
+ VaultItemListingState.ItemListingType.Vault.Identity to false,
+ VaultItemListingState.ItemListingType.Vault.Trash to true,
+ VaultItemListingState.ItemListingType.Vault.Folder(folderId = "mockId-1") to false,
+ VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId-1") to false,
+ VaultItemListingState.ItemListingType.Vault.SshKey to false,
+ )
+ .forEach { (type, expected) ->
+ val result = cipherView.determineListingPredicate(
+ itemListingType = type,
+ )
+ assertEquals(
+ expected,
+ result,
+ )
+ }
+ }
+
+ @Test
+ @Suppress("MaxLineLength")
+ fun `determineListingPredicate should return the correct predicate for a non trash SshKey cipherView`() {
+ val cipherView = createMockCipherView(
+ number = 1,
+ isDeleted = false,
+ cipherType = CipherType.SSH_KEY,
+ )
+
+ mapOf(
+ VaultItemListingState.ItemListingType.Vault.Login to false,
+ VaultItemListingState.ItemListingType.Vault.Card to false,
+ VaultItemListingState.ItemListingType.Vault.SecureNote to false,
+ VaultItemListingState.ItemListingType.Vault.Identity to false,
+ VaultItemListingState.ItemListingType.Vault.Trash to false,
+ VaultItemListingState.ItemListingType.Vault.SshKey to true,
+ VaultItemListingState.ItemListingType.Vault.Folder(folderId = "mockId-1") to true,
+ VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId-1") to true,
+ )
+ .forEach { (type, expected) ->
+ val result = cipherView.determineListingPredicate(
+ itemListingType = type,
+ )
+ assertEquals(
+ expected,
+ result,
+ )
+ }
+ }
+
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for item not in a folder`() {
@@ -873,15 +933,76 @@ class VaultItemListingDataExtensionsTest {
createMockCollectionView(number = 3),
)
- val result = VaultItemListingState.ItemListingType.Vault.Login
- .updateWithAdditionalDataIfNecessary(
- folderList = folderViewList,
- collectionList = collectionViewList,
- )
+ assertEquals(
+ VaultItemListingState.ItemListingType.Vault.Identity,
+ VaultItemListingState.ItemListingType.Vault.Identity
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
+ )
assertEquals(
VaultItemListingState.ItemListingType.Vault.Login,
- result,
+ VaultItemListingState.ItemListingType.Vault.Login
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
+ )
+
+ assertEquals(
+ VaultItemListingState.ItemListingType.Vault.SecureNote,
+ VaultItemListingState.ItemListingType.Vault.SecureNote
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
+ )
+
+ assertEquals(
+ VaultItemListingState.ItemListingType.Vault.Trash,
+ VaultItemListingState.ItemListingType.Vault.Trash
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
+ )
+
+ assertEquals(
+ VaultItemListingState.ItemListingType.Vault.SshKey,
+ VaultItemListingState.ItemListingType.Vault.SshKey
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
+ )
+
+ assertEquals(
+ VaultItemListingState.ItemListingType.Vault.Card,
+ VaultItemListingState.ItemListingType.Vault.Card
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
+ )
+
+ assertEquals(
+ VaultItemListingState.ItemListingType.Send.SendFile,
+ VaultItemListingState.ItemListingType.Send.SendFile
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
+ )
+
+ assertEquals(
+ VaultItemListingState.ItemListingType.Send.SendText,
+ VaultItemListingState.ItemListingType.Send.SendText
+ .updateWithAdditionalDataIfNecessary(
+ folderList = folderViewList,
+ collectionList = collectionViewList,
+ ),
)
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt
index 61fa4bb8d..454b20800 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt
@@ -202,6 +202,44 @@ fun createMockDisplayItemForCipher(
isTotp = false,
)
}
+
+ CipherType.SSH_KEY -> {
+ VaultItemListingState.DisplayItem(
+ id = "mockId-$number",
+ title = "mockName-$number",
+ titleTestTag = "CipherNameLabel",
+ secondSubtitle = null,
+ secondSubtitleTestTag = secondSubtitleTestTag,
+ subtitle = subtitle,
+ subtitleTestTag = "CipherSubTitleLabel",
+ iconData = IconData.Local(R.drawable.ic_ssh_key),
+ extraIconList = listOf(
+ IconRes(
+ iconRes = R.drawable.ic_collections,
+ contentDescription = R.string.collections.asText(),
+ testTag = "CipherInCollectionIcon",
+ ),
+ IconRes(
+ iconRes = R.drawable.ic_paperclip,
+ contentDescription = R.string.attachments.asText(),
+ testTag = "CipherWithAttachmentsIcon",
+ ),
+ ),
+ overflowOptions = listOf(
+ ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
+ ListingItemOverflowAction.VaultAction.EditClick(
+ cipherId = "mockId-$number",
+ requiresPasswordReprompt = requiresPasswordReprompt,
+ ),
+ ),
+ optionsTestTag = "CipherOptionsButton",
+ isAutofill = false,
+ isFido2Creation = false,
+ shouldShowMasterPasswordReprompt = false,
+ iconTestTag = "SshKeyCipherIcon",
+ isTotp = false,
+ )
+ }
}
/**
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt
index 0485bf6ee..211d6dc58 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt
@@ -111,6 +111,16 @@ class VaultItemListingStateExtensionsTest {
assertEquals(expected, result)
}
+ @Test
+ fun `toSearchType should return SshKey when item type is SshKey`() {
+ val expected = SearchType.Vault.SshKeys
+ val itemType = VaultItemListingState.ItemListingType.Vault.SshKey
+
+ val result = itemType.toSearchType()
+
+ assertEquals(expected, result)
+ }
+
@Test
fun `toVaultItemCipherType should return the correct response`() {
val itemListingTypes = listOf(
@@ -119,6 +129,7 @@ class VaultItemListingStateExtensionsTest {
VaultItemListingState.ItemListingType.Vault.SecureNote,
VaultItemListingState.ItemListingType.Vault.Login,
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId"),
+ VaultItemListingState.ItemListingType.Vault.SshKey,
)
val result = itemListingTypes.map { it.toVaultItemCipherType() }
@@ -130,6 +141,7 @@ class VaultItemListingStateExtensionsTest {
VaultItemCipherType.SECURE_NOTE,
VaultItemCipherType.LOGIN,
VaultItemCipherType.LOGIN,
+ VaultItemCipherType.SSH_KEY,
),
result,
)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensionsTest.kt
index c29d7fe74..571c51dc8 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingTypeExtensionsTest.kt
@@ -14,6 +14,13 @@ class VaultItemListingTypeExtensionsTest {
VaultItemListingType.Folder(folderId = "mock"),
VaultItemListingType.Trash,
VaultItemListingType.Collection(collectionId = "collectionId"),
+ VaultItemListingType.SshKey,
+ VaultItemListingType.SendFile,
+ VaultItemListingType.SendText,
+ VaultItemListingType.Card,
+ VaultItemListingType.Identity,
+ VaultItemListingType.Login,
+ VaultItemListingType.SecureNote,
)
val result = itemListingTypeList.map { it.toItemListingType() }
@@ -25,6 +32,13 @@ class VaultItemListingTypeExtensionsTest {
VaultItemListingState.ItemListingType.Vault.Collection(
collectionId = "collectionId",
),
+ VaultItemListingState.ItemListingType.Vault.SshKey,
+ VaultItemListingState.ItemListingType.Send.SendFile,
+ VaultItemListingState.ItemListingType.Send.SendText,
+ VaultItemListingState.ItemListingType.Vault.Card,
+ VaultItemListingState.ItemListingType.Vault.Identity,
+ VaultItemListingState.ItemListingType.Vault.Login,
+ VaultItemListingState.ItemListingType.Vault.SecureNote,
),
result,
)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt
index 157d01688..203f9a8fe 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt
@@ -9,6 +9,8 @@ import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasScrollToNodeAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
+import androidx.compose.ui.test.isDisplayed
+import androidx.compose.ui.test.isNotDisplayed
import androidx.compose.ui.test.isPopup
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
@@ -704,6 +706,13 @@ class VaultScreenTest : BaseComposeTest() {
assertEquals(VaultItemListingType.SecureNote, onNavigateToVaultItemListingType)
}
+ @Test
+ @Suppress("MaxLineLength")
+ fun `NavigateToItemListing event for SshKey type should call onNavigateToVaultItemListingType with SshKey type`() {
+ mutableEventFlow.tryEmit(VaultEvent.NavigateToItemListing(VaultItemListingType.SshKey))
+ assertEquals(VaultItemListingType.SshKey, onNavigateToVaultItemListingType)
+ }
+
@Test
@Suppress("MaxLineLength")
fun `NavigateToItemListing event for Trash type should call onNavigateToVaultItemListingType with Trash type`() {
@@ -1207,6 +1216,62 @@ class VaultScreenTest : BaseComposeTest() {
mutableEventFlow.tryEmit(VaultEvent.ShowSnackbar(data))
composeTestRule.onNodeWithText("message").assertIsDisplayed()
}
+
+ @Test
+ fun `SSH key group header should display correctly based on state`() {
+ val count = 1
+ // Verify SSH key group is displayed when showSshKeys is true
+ mutableStateFlow.update {
+ it.copy(
+ showSshKeys = true,
+ viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
+ sshKeyItemsCount = count,
+ ),
+ )
+ }
+ composeTestRule
+ .onNodeWithText("SSH key")
+ .assertTextEquals("SSH key", count.toString())
+ .assertIsDisplayed()
+
+ // Verify SSH key group is hidden when showSshKeys is false
+ mutableStateFlow.update { it.copy(showSshKeys = false) }
+ composeTestRule
+ .onNodeWithText("SSH key")
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun `SSH key vault items should display correctly based on state`() {
+ // Verify SSH key vault items are displayed when showSshKeys is true
+ mutableStateFlow.update {
+ it.copy(
+ showSshKeys = true,
+ viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
+ noFolderItems = listOf(
+ VaultState.ViewState.VaultItem.SshKey(
+ id = "mockId",
+ name = "mockSshKey".asText(),
+ publicKey = "mockPublicKey".asText(),
+ privateKey = "mockPrivateKey".asText(),
+ fingerprint = "mockFingerprint".asText(),
+ overflowOptions = emptyList(),
+ shouldShowMasterPasswordReprompt = false,
+ ),
+ ),
+ ),
+ )
+ }
+ composeTestRule
+ .onNodeWithTextAfterScroll("mockSshKey")
+ .isDisplayed()
+
+ // Verify SSH key vault items are hidden when showSshKeys is false
+ mutableStateFlow.update { it.copy(showSshKeys = false) }
+ composeTestRule
+ .onNodeWithText("mockSshKey")
+ .isNotDisplayed()
+ }
}
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
@@ -1262,6 +1327,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
hideNotificationsDialog = true,
isRefreshing = false,
showImportActionCard = false,
+ showSshKeys = false,
)
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(
@@ -1275,4 +1341,6 @@ private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultStat
collectionItems = emptyList(),
trashItemsCount = 0,
totpItemsCount = 0,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
index 509b3bba5..d3071498d 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
import app.cash.turbine.test
+import com.bitwarden.vault.CipherType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -124,10 +125,17 @@ class VaultViewModelTest : BaseViewModelTest() {
}
private val mutableImportLoginsFeatureFlow = MutableStateFlow(true)
+ private val mutableSshKeyVaultItemsEnabledFlow = MutableStateFlow(false)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
} returns mutableImportLoginsFeatureFlow
+ every {
+ getFeatureFlagFlow(FlagKey.SshKeyCipherItems)
+ } returns mutableSshKeyVaultItemsEnabledFlow
+ every {
+ getFeatureFlag(FlagKey.SshKeyCipherItems)
+ } returns mutableSshKeyVaultItemsEnabledFlow.value
}
@Test
@@ -526,6 +534,7 @@ class VaultViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
hasMasterPassword = true,
+ showSshKeys = false,
),
)
.copy(
@@ -550,6 +559,7 @@ class VaultViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
hasMasterPassword = true,
+ showSshKeys = false,
),
),
viewModel.stateFlow.value,
@@ -559,12 +569,31 @@ class VaultViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
+ mutableSshKeyVaultItemsEnabledFlow.value = true
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
- cipherViewList = listOf(createMockCipherView(number = 1)),
- collectionViewList = listOf(createMockCollectionView(number = 1)),
- folderViewList = listOf(createMockFolderView(number = 1)),
+ cipherViewList = listOf(
+ createMockCipherView(number = 1, cipherType = CipherType.LOGIN),
+ createMockCipherView(number = 2, cipherType = CipherType.CARD),
+ createMockCipherView(number = 3, cipherType = CipherType.IDENTITY),
+ createMockCipherView(number = 4, cipherType = CipherType.SECURE_NOTE),
+ createMockCipherView(number = 5, cipherType = CipherType.SSH_KEY),
+ ),
+ collectionViewList = listOf(
+ createMockCollectionView(number = 1),
+ createMockCollectionView(number = 2),
+ createMockCollectionView(number = 3),
+ createMockCollectionView(number = 4),
+ createMockCollectionView(number = 5),
+ ),
+ folderViewList = listOf(
+ createMockFolderView(number = 1),
+ createMockFolderView(number = 2),
+ createMockFolderView(number = 3),
+ createMockFolderView(number = 4),
+ createMockFolderView(number = 5),
+ ),
sendViewList = listOf(createMockSendView(number = 1)),
),
),
@@ -576,9 +605,9 @@ class VaultViewModelTest : BaseViewModelTest() {
createMockVaultState(
viewState = VaultState.ViewState.Content(
loginItemsCount = 1,
- cardItemsCount = 0,
- identityItemsCount = 0,
- secureNoteItemsCount = 0,
+ cardItemsCount = 1,
+ identityItemsCount = 1,
+ secureNoteItemsCount = 1,
favoriteItems = listOf(),
folderItems = listOf(
VaultState.ViewState.FolderItem(
@@ -586,6 +615,26 @@ class VaultViewModelTest : BaseViewModelTest() {
name = "mockName-1".asText(),
itemCount = 1,
),
+ VaultState.ViewState.FolderItem(
+ id = "mockId-2",
+ name = "mockName-2".asText(),
+ itemCount = 1,
+ ),
+ VaultState.ViewState.FolderItem(
+ id = "mockId-3",
+ name = "mockName-3".asText(),
+ itemCount = 1,
+ ),
+ VaultState.ViewState.FolderItem(
+ id = "mockId-4",
+ name = "mockName-4".asText(),
+ itemCount = 1,
+ ),
+ VaultState.ViewState.FolderItem(
+ id = "mockId-5",
+ name = "mockName-5".asText(),
+ itemCount = 1,
+ ),
),
collectionItems = listOf(
VaultState.ViewState.CollectionItem(
@@ -593,11 +642,34 @@ class VaultViewModelTest : BaseViewModelTest() {
name = "mockName-1",
itemCount = 1,
),
+ VaultState.ViewState.CollectionItem(
+ id = "mockId-2",
+ name = "mockName-2",
+ itemCount = 1,
+ ),
+ VaultState.ViewState.CollectionItem(
+ id = "mockId-3",
+ name = "mockName-3",
+ itemCount = 1,
+ ),
+ VaultState.ViewState.CollectionItem(
+ id = "mockId-4",
+ name = "mockName-4",
+ itemCount = 1,
+ ),
+ VaultState.ViewState.CollectionItem(
+ id = "mockId-5",
+ name = "mockName-5",
+ itemCount = 1,
+ ),
),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = CipherType.entries.size,
+ sshKeyItemsCount = 1,
),
+ showSshKeys = true,
),
viewModel.stateFlow.value,
)
@@ -619,6 +691,8 @@ class VaultViewModelTest : BaseViewModelTest() {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
)
val viewModel = createViewModel()
@@ -731,6 +805,8 @@ class VaultViewModelTest : BaseViewModelTest() {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
),
viewModel.stateFlow.value,
@@ -829,6 +905,8 @@ class VaultViewModelTest : BaseViewModelTest() {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
dialog = VaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
@@ -927,6 +1005,8 @@ class VaultViewModelTest : BaseViewModelTest() {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
dialog = VaultState.DialogState.Error(
title = R.string.internet_connection_required_title.asText(),
@@ -999,6 +1079,88 @@ class VaultViewModelTest : BaseViewModelTest() {
)
}
+ @Test
+ fun `vaultDataStateFlow Loaded should exclude SSH key vault items when showSshKeys is false`() =
+ runTest {
+ mutableVaultDataStateFlow.tryEmit(
+ value = DataState.Loaded(
+ data = VaultData(
+ cipherViewList = listOf(
+ createMockCipherView(number = 1),
+ createMockCipherView(number = 1, cipherType = CipherType.SSH_KEY),
+ ),
+ collectionViewList = listOf(),
+ folderViewList = listOf(),
+ sendViewList = listOf(),
+ ),
+ ),
+ )
+
+ val viewModel = createViewModel()
+
+ assertEquals(
+ createMockVaultState(
+ viewState = VaultState.ViewState.Content(
+ loginItemsCount = 1,
+ cardItemsCount = 0,
+ identityItemsCount = 0,
+ secureNoteItemsCount = 0,
+ favoriteItems = listOf(),
+ folderItems = listOf(),
+ collectionItems = listOf(),
+ noFolderItems = listOf(),
+ trashItemsCount = 0,
+ totpItemsCount = 1,
+ itemTypesCount = CipherType.entries.size - 1,
+ sshKeyItemsCount = 0,
+ ),
+ ),
+ viewModel.stateFlow.value,
+ )
+ }
+
+ @Test
+ fun `vaultDataStateFlow Loaded should include SSH key vault items when showSshKeys is true`() =
+ runTest {
+ mutableSshKeyVaultItemsEnabledFlow.value = true
+ mutableVaultDataStateFlow.tryEmit(
+ value = DataState.Loaded(
+ data = VaultData(
+ cipherViewList = listOf(
+ createMockCipherView(number = 1),
+ createMockCipherView(number = 1, cipherType = CipherType.SSH_KEY),
+ ),
+ collectionViewList = listOf(),
+ folderViewList = listOf(),
+ sendViewList = listOf(),
+ ),
+ ),
+ )
+
+ val viewModel = createViewModel()
+
+ assertEquals(
+ createMockVaultState(
+ viewState = VaultState.ViewState.Content(
+ loginItemsCount = 1,
+ cardItemsCount = 0,
+ identityItemsCount = 0,
+ secureNoteItemsCount = 0,
+ favoriteItems = listOf(),
+ folderItems = listOf(),
+ collectionItems = listOf(),
+ noFolderItems = listOf(),
+ trashItemsCount = 0,
+ totpItemsCount = 1,
+ itemTypesCount = CipherType.entries.size,
+ sshKeyItemsCount = 1,
+ ),
+ showSshKeys = true,
+ ),
+ viewModel.stateFlow.value,
+ )
+ }
+
@Test
fun `VerificationCodesClick should emit NavigateToVerificationCodeScreen`() = runTest {
val viewModel = createViewModel()
@@ -1112,6 +1274,18 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
+ @Test
+ fun `SshKeyGroupClick should emit NavigateToItemListing event with SshKey type`() = runTest {
+ val viewModel = createViewModel()
+ viewModel.eventFlow.test {
+ viewModel.trySendAction(VaultAction.SshKeyGroupClick)
+ assertEquals(
+ VaultEvent.NavigateToItemListing(VaultItemListingType.SshKey),
+ awaitItem(),
+ )
+ }
+ }
+
@Test
fun `TrashClick should emit NavigateToItemListing event with Trash type`() = runTest {
val viewModel = createViewModel()
@@ -1720,6 +1894,7 @@ private val DEFAULT_USER_STATE = UserState(
private fun createMockVaultState(
viewState: VaultState.ViewState,
dialog: VaultState.DialogState? = null,
+ showSshKeys: Boolean = false,
): VaultState =
VaultState(
appBarTitle = R.string.my_vault.asText(),
@@ -1758,4 +1933,5 @@ private fun createMockVaultState(
hideNotificationsDialog = true,
showImportActionCard = true,
isRefreshing = false,
+ showSshKeys = showSshKeys,
)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt
index 43c4d4be4..6430fc086 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt
@@ -12,6 +12,7 @@ import com.bitwarden.vault.LoginView
import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SecureNoteView
+import com.bitwarden.vault.SshKeyView
import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
@@ -112,6 +113,7 @@ class VaultAddItemStateExtensionsTest {
creationDate = Instant.MIN,
deletedDate = null,
revisionDate = Instant.MIN,
+ sshKey = null,
),
result,
)
@@ -295,6 +297,7 @@ class VaultAddItemStateExtensionsTest {
creationDate = Instant.MIN,
deletedDate = null,
revisionDate = Instant.MIN,
+ sshKey = null,
),
result,
)
@@ -426,6 +429,7 @@ class VaultAddItemStateExtensionsTest {
creationDate = Instant.MIN,
deletedDate = null,
revisionDate = Instant.MIN,
+ sshKey = null,
),
result,
)
@@ -611,6 +615,7 @@ class VaultAddItemStateExtensionsTest {
creationDate = Instant.MIN,
deletedDate = null,
revisionDate = Instant.MIN,
+ sshKey = null,
),
result,
)
@@ -703,6 +708,65 @@ class VaultAddItemStateExtensionsTest {
)
}
+ @Test
+ fun `toCipherView should transform SSH Key ItemType to CipherView`() {
+ mockkStatic(Instant::class)
+ every { Instant.now() } returns Instant.MIN
+ val viewState = VaultAddEditState.ViewState.Content(
+ common = VaultAddEditState.ViewState.Content.Common(
+ name = "mockName-1",
+ selectedFolderId = "mockId-1",
+ favorite = false,
+ masterPasswordReprompt = false,
+ notes = "mockNotes-1",
+ selectedOwnerId = "mockOwnerId-1",
+ ),
+ isIndividualVaultDisabled = false,
+ type = VaultAddEditState.ViewState.Content.ItemType.SshKey(
+ publicKey = "mockPublicKey-1",
+ privateKey = "mockPrivateKey-1",
+ fingerprint = "mockFingerprint-1",
+ ),
+ )
+
+ val result = viewState.toCipherView()
+
+ assertEquals(
+ CipherView(
+ id = null,
+ organizationId = "mockOwnerId-1",
+ folderId = "mockId-1",
+ collectionIds = emptyList(),
+ key = null,
+ name = "mockName-1",
+ notes = "mockNotes-1",
+ type = CipherType.SSH_KEY,
+ login = null,
+ identity = null,
+ card = null,
+ secureNote = null,
+ favorite = false,
+ reprompt = CipherRepromptType.NONE,
+ organizationUseTotp = false,
+ edit = true,
+ viewPassword = true,
+ localData = null,
+ attachments = null,
+ fields = emptyList(),
+ passwordHistory = null,
+ creationDate = Instant.MIN,
+ deletedDate = null,
+ revisionDate = Instant.MIN,
+ sshKey = SshKeyView(
+ publicKey = "mockPublicKey-1",
+ privateKey = "mockPrivateKey-1",
+ fingerprint = "mockFingerprint-1",
+ ),
+ ),
+ result,
+ )
+ }
+
@Suppress("MaxLineLength")
@Test
fun `toLoginView should transform Login ItemType to LoginView deleting fido2Credentials with original cipher`() {
@@ -911,6 +975,7 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
creationDate = Instant.MIN,
deletedDate = null,
revisionDate = Instant.MIN,
+ sshKey = null,
)
private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt
index 5ea4c6e64..17e257de1 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt
@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import android.net.Uri
+import com.bitwarden.vault.CipherRepromptType
import com.bitwarden.vault.CipherType
import com.bitwarden.vault.FolderView
import com.bitwarden.vault.LoginUriView
@@ -12,9 +13,12 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
+import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSshKeyView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
+import com.x8bit.bitwarden.ui.platform.components.model.IconRes
+import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import io.mockk.every
@@ -57,6 +61,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -94,6 +99,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -118,6 +125,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.MyVault,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -138,6 +146,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -171,6 +181,7 @@ class VaultDataExtensionsTest {
organizationName = "Mock Organization 1",
),
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -197,6 +208,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -217,6 +230,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -240,6 +254,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -264,6 +279,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -278,6 +294,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -299,6 +317,7 @@ class VaultDataExtensionsTest {
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -313,6 +332,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 0,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -334,6 +355,7 @@ class VaultDataExtensionsTest {
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -348,6 +370,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -371,6 +395,7 @@ class VaultDataExtensionsTest {
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -385,6 +410,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -592,6 +619,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -606,6 +634,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 2,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -629,6 +659,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -643,6 +674,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 2,
totpItemsCount = 0,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -669,6 +702,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -689,6 +723,8 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 100,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
),
actual,
)
@@ -722,6 +758,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
+ showSshKeys = false,
)
assertEquals(
@@ -764,8 +801,195 @@ class VaultDataExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
+ ),
+ actual,
+ )
+ }
+
+ @Test
+ fun `toViewState should exclude SSH keys if showSshKeys is false`() {
+ val vaultData = VaultData(
+ cipherViewList = listOf(
+ createMockCipherView(number = 1),
+ createMockCipherView(number = 2, cipherType = CipherType.SSH_KEY),
+ ),
+ collectionViewList = listOf(),
+ folderViewList = listOf(),
+ sendViewList = listOf(),
+ fido2CredentialAutofillViewList = null,
+ )
+
+ val actual = vaultData.toViewState(
+ isPremium = true,
+ isIconLoadingDisabled = false,
+ baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
+ vaultFilterType = VaultFilterType.AllVaults,
+ hasMasterPassword = true,
+ showSshKeys = false,
+ )
+
+ assertEquals(
+ VaultState.ViewState.Content(
+ loginItemsCount = 1,
+ cardItemsCount = 0,
+ identityItemsCount = 0,
+ secureNoteItemsCount = 0,
+ // Verify SSH key vault items are not counted when showSshKeys is false.
+ sshKeyItemsCount = 0,
+ favoriteItems = listOf(),
+ collectionItems = listOf(),
+ folderItems = listOf(),
+ noFolderItems = listOf(),
+ trashItemsCount = 0,
+ totpItemsCount = 1,
+ // Verify item types count excludes CipherType.SSH_KEY when showSshKeys is false.
+ itemTypesCount = 4,
+ ),
+ actual,
+ )
+ }
+
+ @Test
+ fun `toViewState should include SSH key vault items and type count if showSshKeys is true`() {
+ val vaultData = VaultData(
+ cipherViewList = listOf(
+ createMockCipherView(number = 1),
+ createMockCipherView(number = 2, cipherType = CipherType.SSH_KEY),
+ ),
+ collectionViewList = listOf(),
+ folderViewList = listOf(),
+ sendViewList = listOf(),
+ fido2CredentialAutofillViewList = null,
+ )
+
+ val actual = vaultData.toViewState(
+ isPremium = true,
+ isIconLoadingDisabled = false,
+ baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
+ vaultFilterType = VaultFilterType.AllVaults,
+ hasMasterPassword = true,
+ showSshKeys = true,
+ )
+
+ assertEquals(
+ VaultState.ViewState.Content(
+ loginItemsCount = 1,
+ cardItemsCount = 0,
+ identityItemsCount = 0,
+ secureNoteItemsCount = 0,
+ // Verify SSH key vault items are counted
+ sshKeyItemsCount = 1,
+ favoriteItems = listOf(),
+ collectionItems = listOf(),
+ folderItems = listOf(),
+ noFolderItems = listOf(),
+ trashItemsCount = 0,
+ totpItemsCount = 1,
+ // Verify item types count includes all CipherTypes when showSshKeys is true.
+ itemTypesCount = CipherType.entries.size,
+ ),
+ actual,
+ )
+ }
+
+ @Test
+ fun `toViewState should transform SSH key vault items into correct vault item`() {
+ val vaultData = VaultData(
+ cipherViewList = listOf(
+ createMockCipherView(number = 1, cipherType = CipherType.SSH_KEY, folderId = null),
+ createMockCipherView(
+ number = 2,
+ cipherType = CipherType.SSH_KEY,
+ repromptType = CipherRepromptType.PASSWORD,
+ folderId = null,
+ sshKey = createMockSshKeyView(number = 1)
+ .copy(
+ publicKey = null,
+ privateKey = null,
+ fingerprint = null,
+ ),
+ ),
+ createMockCipherView(
+ number = 3,
+ cipherType = CipherType.SSH_KEY,
+ folderId = null,
+ sshKey = null,
+ ),
+ ),
+ collectionViewList = listOf(),
+ folderViewList = listOf(),
+ sendViewList = listOf(),
+ )
+ val actual = vaultData.toViewState(
+ isPremium = true,
+ isIconLoadingDisabled = false,
+ baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
+ vaultFilterType = VaultFilterType.AllVaults,
+ hasMasterPassword = true,
+ showSshKeys = true,
+ )
+
+ assertEquals(
+ VaultState.ViewState.Content(
+ loginItemsCount = 0,
+ cardItemsCount = 0,
+ identityItemsCount = 0,
+ secureNoteItemsCount = 0,
+ sshKeyItemsCount = 3,
+ favoriteItems = listOf(),
+ collectionItems = listOf(),
+ folderItems = listOf(),
+ noFolderItems = listOf(
+ createMockSshKeyVaultItem(number = 1),
+ createMockSshKeyVaultItem(number = 2)
+ .copy(
+ publicKey = null,
+ privateKey = null,
+ fingerprint = null,
+ shouldShowMasterPasswordReprompt = true,
+ ),
+ createMockSshKeyVaultItem(number = 3)
+ .copy(
+ publicKey = null,
+ privateKey = null,
+ fingerprint = null,
+ ),
+ ),
+ trashItemsCount = 0,
+ totpItemsCount = 0,
+ itemTypesCount = CipherType.entries.size,
),
actual,
)
}
}
+
+private fun createMockSshKeyVaultItem(number: Int): VaultState.ViewState.VaultItem.SshKey =
+ VaultState.ViewState.VaultItem.SshKey(
+ id = "mockId-$number",
+ name = "mockName-$number".asText(),
+ publicKey = "mockPublicKey-$number".asText(),
+ privateKey = "mockPrivateKey-$number".asText(),
+ fingerprint = "mockKeyFingerprint-$number".asText(),
+ overflowOptions = listOf(
+ ListingItemOverflowAction.VaultAction.ViewClick("mockId-$number"),
+ ListingItemOverflowAction.VaultAction.EditClick("mockId-$number", true),
+ ),
+ startIcon = IconData.Local(iconRes = R.drawable.ic_ssh_key),
+ startIconTestTag = "SshKeyCipherIcon",
+ extraIconList = listOf(
+ IconRes(
+ iconRes = R.drawable.ic_collections,
+ contentDescription = R.string.collections.asText(),
+ testTag = "CipherInCollectionIcon",
+ ),
+ IconRes(
+ iconRes = R.drawable.ic_paperclip,
+ contentDescription = R.string.attachments.asText(),
+ testTag = "CipherWithAttachmentsIcon",
+ ),
+ ),
+ shouldShowMasterPasswordReprompt = false,
+ )
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultStateExtensionsTest.kt
index 6d275b9eb..47ab815fb 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultStateExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultStateExtensionsTest.kt
@@ -83,5 +83,7 @@ class VaultStateExtensionsTest {
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
+ itemTypesCount = 4,
+ sshKeyItemsCount = 0,
)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ca783f41e..2049224d1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,7 +24,7 @@ androidxSplash = "1.1.0-rc01"
androidXAppCompat = "1.7.0"
androdixAutofill = "1.1.0"
androidxWork = "2.9.1"
-bitwardenSdk = "1.0.0-20240924.112512-21"
+bitwardenSdk = "1.0.0-20241021.160919-71"
crashlytics = "3.0.2"
detekt = "1.23.7"
firebaseBom = "33.5.1"