From d2ffd7bf016f61b0f5100be03ea6977c505e8292 Mon Sep 17 00:00:00 2001 From: Oleg Semenenko <146032743+oleg-livefront@users.noreply.github.com> Date: Mon, 29 Jan 2024 22:03:56 -0600 Subject: [PATCH] BIT-1653 Add functionality to new URI button (#862) --- .../feature/addedit/VaultAddEditLoginItems.kt | 8 ++-- .../feature/addedit/VaultAddEditViewModel.kt | 32 ++++++++++++---- .../handlers/VaultAddEditLoginTypeHandlers.kt | 3 +- .../ui/vault/feature/addedit/model/UriItem.kt | 15 ++++++++ .../addedit/util/CipherViewExtensions.kt | 23 +++++++++++- .../vault/util/VaultAddItemStateExtensions.kt | 17 ++++----- .../feature/addedit/VaultAddEditScreenTest.kt | 17 +++++++-- .../addedit/VaultAddEditViewModelTest.kt | 37 ++++++++++++------- .../addedit/util/CipherViewExtensionsTest.kt | 3 +- .../util/VaultAddItemStateExtensionsTest.kt | 7 ++-- 10 files changed, 119 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/UriItem.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt index 14b79016ae..fd6407c552 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt @@ -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( 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 8264b7cdb3..5848ce0813 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 @@ -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 = 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. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt index 2ff0f7a285..ce53fe6aa8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/UriItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/UriItem.kt new file mode 100644 index 0000000000..c67a57a466 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/UriItem.kt @@ -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 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 0c45bf4b1d..a32706a79d 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 @@ -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?.toUriItems(): List = + 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, + ) + } + } 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 7ad7718cdc..146854e807 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 @@ -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?.toLoginUriView(): List? = + this + ?.filter { it.uri?.isNotBlank() == true } + ?.map { LoginUriView(uri = it.uri.orEmpty(), match = it.match) } + .takeUnless { it.isNullOrEmpty() } 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 521f7dbb28..c7944d33d6 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 @@ -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 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 5b91778109..37da41547a 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 @@ -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 = 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, ) 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 9a5cce6b66..57abbe9021 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 @@ -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, ), 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 a3d5cae24d..409d430ae2 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 @@ -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",