BIT-1653 Add functionality to new URI button (#862)

This commit is contained in:
Oleg Semenenko 2024-01-29 22:03:56 -06:00 committed by Álison Fernandes
parent b1cc9a1dd6
commit d2ffd7bf01
10 changed files with 119 additions and 43 deletions

View file

@ -153,12 +153,14 @@ fun LazyListScope.vaultAddEditLoginItems(
)
}
item {
items(loginState.uriList) { uriItem ->
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.uri),
value = loginState.uri,
onValueChange = loginItemTypeHandlers.onUriTextChange,
value = uriItem.uri.orEmpty(),
onValueChange = {
loginItemTypeHandlers.onUriTextChange(uriItem.copy(uri = it))
},
actions = {
BitwardenIconButtonWithResource(
iconRes = IconResource(

View file

@ -25,6 +25,7 @@ import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
@ -43,6 +44,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.Collections
import java.util.UUID
import javax.inject.Inject
private const val KEY_STATE = "state"
@ -545,7 +547,17 @@ class VaultAddEditViewModel @Inject constructor(
action: VaultAddEditAction.ItemType.LoginType.UriTextChange,
) {
updateLoginContent { loginType ->
loginType.copy(uri = action.uri)
loginType.copy(
uriList = loginType
.uriList
.map { uriItem ->
if (uriItem.id == action.uri.id) {
action.uri
} else {
uriItem
}
},
)
}
}
@ -603,10 +615,12 @@ class VaultAddEditViewModel @Inject constructor(
}
private fun handleLoginAddNewUriClick() {
viewModelScope.launch {
sendEvent(
event = VaultAddEditEvent.ShowToast(
message = "Add New URI".asText(),
updateLoginContent { loginType ->
loginType.copy(
uriList = loginType.uriList + UriItem(
id = UUID.randomUUID().toString(),
uri = "",
match = null,
),
)
}
@ -1360,7 +1374,7 @@ data class VaultAddEditState(
*
* @property username The username required for the login item.
* @property password The password required for the login item.
* @property uri The URI associated with the login item.
* @property uriList The list of URIs associated with the login item.
* @property totp The current TOTP (if applicable).
* @property canViewPassword Indicates whether the current user can view and copy
* passwords associated with the login item.
@ -1369,9 +1383,11 @@ data class VaultAddEditState(
data class Login(
val username: String = "",
val password: String = "",
val uri: String = "",
val totp: String? = null,
val canViewPassword: Boolean = true,
val uriList: List<UriItem> = listOf(
UriItem(id = UUID.randomUUID().toString(), uri = "", match = null),
),
) : ItemType() {
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.LOGIN
}
@ -1744,7 +1760,7 @@ sealed class VaultAddEditAction {
*
* @property uri The new URI text.
*/
data class UriTextChange(val uri: String) : LoginType()
data class UriTextChange(val uri: UriItem) : LoginType()
/**
* Represents the action to set up TOTP.

View file

@ -2,6 +2,7 @@ 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
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
/**
* A collection of handler functions specifically tailored for managing actions
@ -27,7 +28,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
data class VaultAddEditLoginTypeHandlers(
val onUsernameTextChange: (String) -> Unit,
val onPasswordTextChange: (String) -> Unit,
val onUriTextChange: (String) -> Unit,
val onUriTextChange: (UriItem) -> Unit,
val onOpenUsernameGeneratorClick: () -> Unit,
val onPasswordCheckerClick: () -> Unit,
val onOpenPasswordGeneratorClick: () -> Unit,

View file

@ -0,0 +1,15 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.model
import android.os.Parcelable
import com.bitwarden.core.UriMatchType
import kotlinx.parcelize.Parcelize
/**
* Represents the URI item being displayed to the user.
*/
@Parcelize
data class UriItem(
val id: String,
val uri: String?,
val match: UriMatchType?,
) : Parcelable

View file

@ -5,10 +5,12 @@ import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.FieldType
import com.bitwarden.core.FieldView
import com.bitwarden.core.LoginUriView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
@ -29,7 +31,7 @@ fun CipherView.toViewState(
VaultAddEditState.ViewState.Content.ItemType.Login(
username = login?.username.orEmpty(),
password = login?.password.orEmpty(),
uri = login?.uris?.firstOrNull()?.uri.orEmpty(),
uriList = login?.uris.toUriItems(),
totp = login?.totp,
canViewPassword = this.viewPassword,
)
@ -138,3 +140,22 @@ private fun String.appendCloneTextIfRequired(
} else {
this
}
private fun List<LoginUriView>?.toUriItems(): List<UriItem> =
if (this.isNullOrEmpty()) {
listOf(
UriItem(
id = UUID.randomUUID().toString(),
uri = "",
match = null,
),
)
} else {
this.map { loginUriView ->
UriItem(
id = UUID.randomUUID().toString(),
uri = loginUriView.uri,
match = loginUriView.match,
)
}
}

View file

@ -11,9 +11,9 @@ import com.bitwarden.core.LoginUriView
import com.bitwarden.core.LoginView
import com.bitwarden.core.SecureNoteType
import com.bitwarden.core.SecureNoteView
import com.bitwarden.core.UriMatchType
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
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
@ -124,14 +124,7 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toLoginView(
username = it.username,
password = it.password,
passwordRevisionDate = common.originalCipher?.login?.passwordRevisionDate,
uris = listOf(
// TODO Implement URI list (BIT-1094)
LoginUriView(
uri = it.uri,
// TODO Implement URI settings in (BIT-1094)
match = UriMatchType.DOMAIN,
),
),
uris = it.uriList.toLoginUriView(),
totp = it.totp,
autofillOnPageLoad = common.originalCipher?.login?.autofillOnPageLoad,
)
@ -190,3 +183,9 @@ private fun VaultAddEditState.Custom.toFieldView(): FieldView =
)
}
}
private fun List<UriItem>?.toLoginUriView(): List<LoginUriView>? =
this
?.filter { it.uri?.isNotBlank() == true }
?.map { LoginUriView(uri = it.uri.orEmpty(), match = it.match) }
.takeUnless { it.isNullOrEmpty() }

View file

@ -43,6 +43,7 @@ import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
@ -720,13 +721,21 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Test
fun `in ItemType_Login state changing URI text field should trigger UriTextChange`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(uriList = listOf(UriItem("TestId", "URI", null)))
}
}
composeTestRule
.onNodeWithTextAfterScroll("URI")
.performTextInput("TestURI")
.performTextInput("Test")
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.UriTextChange("TestURI"),
VaultAddEditAction.ItemType.LoginType.UriTextChange(
UriItem("TestId", "TestURI", null),
),
)
}
}
@ -738,7 +747,9 @@ class VaultAddEditScreenTest : BaseComposeTest() {
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(uri = "NewURI") }
updateLoginType(currentState) {
copy(uriList = listOf(UriItem("TestId", "NewURI", null)))
}
}
composeTestRule

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.CipherView
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
@ -24,6 +25,7 @@ import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
@ -306,7 +308,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
typeContentViewState = createLoginTypeContentViewState(
username = "mockUsername-1",
password = "mockPassword-1",
uri = "www.mockuri1.com",
uri = listOf(UriItem("testId", "www.mockuri1.com", UriMatchType.HOST)),
totpCode = "mockTotp-1",
canViewPassword = false,
),
@ -755,13 +757,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Test
fun `UriTextChange should update uri in LoginItem`() = runTest {
val action = VaultAddEditAction.ItemType.LoginType.UriTextChange("newUri")
val action = VaultAddEditAction.ItemType.LoginType.UriTextChange(
UriItem("testId", "TestUri", null),
)
viewModel.actionChannel.trySend(action)
val expectedState = createVaultAddItemState(
typeContentViewState = createLoginTypeContentViewState(
uri = "newUri",
uri = listOf(UriItem("testId", "TestUri", null)),
),
)
@ -985,18 +989,23 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
@Test
fun `AddNewUriClick should emit ShowToast with 'Add New URI' message`() = runTest {
fun `AddNewUriClick should update state with another empty UriItem`() = runTest {
val viewModel = createAddVaultItemViewModel()
every { UUID.randomUUID().toString() } returns "testId2"
viewModel.eventFlow.test {
viewModel
.actionChannel
.trySend(
VaultAddEditAction.ItemType.LoginType.AddNewUriClick,
)
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.AddNewUriClick)
assertEquals(VaultAddEditEvent.ShowToast("Add New URI".asText()), awaitItem())
}
val expectedState = createVaultAddItemState(
typeContentViewState = createLoginTypeContentViewState().copy(
uriList = listOf(UriItem("testId", "", null), UriItem("testId2", "", null)),
),
)
assertEquals(
expectedState,
viewModel.stateFlow.value,
)
}
}
@ -1901,14 +1910,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
private fun createLoginTypeContentViewState(
username: String = "",
password: String = "",
uri: String = "",
uri: List<UriItem> = listOf(UriItem("testId", "", null)),
totpCode: String? = null,
canViewPassword: Boolean = true,
): VaultAddEditState.ViewState.Content.ItemType.Login =
VaultAddEditState.ViewState.Content.ItemType.Login(
username = username,
password = password,
uri = uri,
uriList = uri,
totp = totpCode,
canViewPassword = canViewPassword,
)

View file

@ -16,6 +16,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import io.mockk.every
@ -174,7 +175,7 @@ class CipherViewExtensionsTest {
type = VaultAddEditState.ViewState.Content.ItemType.Login(
username = "username",
password = "password",
uri = "www.example.com",
uriList = listOf(UriItem(TEST_ID, "www.example.com", null)),
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
canViewPassword = false,
),

View file

@ -14,6 +14,7 @@ import com.bitwarden.core.SecureNoteView
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import io.mockk.every
@ -50,7 +51,7 @@ class VaultAddItemStateExtensionsTest {
type = VaultAddEditState.ViewState.Content.ItemType.Login(
username = "mockUsername-1",
password = "mockPassword-1",
uri = "mockUri-1",
uriList = listOf(UriItem("testId", "mockUri-1", UriMatchType.DOMAIN)),
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
),
)
@ -127,7 +128,7 @@ class VaultAddItemStateExtensionsTest {
type = VaultAddEditState.ViewState.Content.ItemType.Login(
username = "mockUsername-1",
password = "mockPassword-1",
uri = "mockUri-1",
uriList = listOf(UriItem("TestId", "mockUri-1", null)),
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
),
)
@ -147,7 +148,7 @@ class VaultAddItemStateExtensionsTest {
uris = listOf(
LoginUriView(
uri = "mockUri-1",
match = UriMatchType.DOMAIN,
match = null,
),
),
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",