BIT-1654 Add URI option menu (#877)

This commit is contained in:
Oleg Semenenko
2024-01-30 18:22:03 -06:00
committed by Álison Fernandes
parent a92d9ff823
commit 95b4aaf605
9 changed files with 493 additions and 67 deletions

View File

@@ -156,22 +156,10 @@ fun LazyListScope.vaultAddEditLoginItems(
items(loginState.uriList) { uriItem ->
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.uri),
value = uriItem.uri.orEmpty(),
onValueChange = {
loginItemTypeHandlers.onUriTextChange(uriItem.copy(uri = it))
},
actions = {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_settings),
contentDescription = stringResource(id = R.string.options),
),
onClick = loginItemTypeHandlers.onUriSettingsClick,
)
},
modifier = Modifier.padding(horizontal = 16.dp),
VaultAddEditUriItem(
uriItem = uriItem,
onUriValueChange = loginItemTypeHandlers.onUriValueChange,
onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick,
)
}

View File

@@ -0,0 +1,99 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialogRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDisplayMatchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toUriMatchType
/**
* The URI item displayed to the user.
*/
@Suppress("LongMethod")
@Composable
fun VaultAddEditUriItem(
uriItem: UriItem,
onUriItemRemoved: (UriItem) -> Unit,
onUriValueChange: (UriItem) -> Unit,
) {
var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowMatchDialog by rememberSaveable { mutableStateOf(false) }
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.uri),
value = uriItem.uri.orEmpty(),
onValueChange = { onUriValueChange(uriItem.copy(uri = it)) },
actions = {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_settings),
contentDescription = stringResource(id = R.string.options),
),
onClick = { shouldShowOptionsDialog = true },
)
},
modifier = Modifier.padding(horizontal = 16.dp),
)
if (shouldShowOptionsDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.options),
onDismissRequest = { shouldShowOptionsDialog = false },
) {
BitwardenBasicDialogRow(
text = stringResource(id = R.string.match_detection),
onClick = {
shouldShowOptionsDialog = false
shouldShowMatchDialog = true
},
)
BitwardenBasicDialogRow(
text = stringResource(id = R.string.remove),
onClick = {
shouldShowOptionsDialog = false
onUriItemRemoved(uriItem)
},
)
}
}
if (shouldShowMatchDialog) {
val selectedString = uriItem.match.toDisplayMatchType().text.invoke()
BitwardenSelectionDialog(
title = stringResource(id = R.string.uri_match_detection),
onDismissRequest = { shouldShowMatchDialog = false },
) {
UriMatchDisplayType
.entries
.forEach { matchType ->
BitwardenSelectionRow(
text = matchType.text,
isSelected = matchType.text.invoke() == selectedString,
onClick = {
shouldShowMatchDialog = false
onUriValueChange(
uriItem.copy(match = matchType.toUriMatchType()),
)
},
)
}
}
}
}

View File

@@ -190,6 +190,7 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.Common.CustomFieldActionSelect -> handleCustomFieldActionSelected(
action,
)
is VaultAddEditAction.Common.CollectionSelect -> handleCollectionSelect(action)
}
}
@@ -542,8 +543,8 @@ class VaultAddEditViewModel @Inject constructor(
handleLoginPasswordTextInputChange(action)
}
is VaultAddEditAction.ItemType.LoginType.UriTextChange -> {
handleLoginUriTextInputChange(action)
is VaultAddEditAction.ItemType.LoginType.UriValueChange -> {
handleLoginUriValueInputChange(action)
}
is VaultAddEditAction.ItemType.LoginType.OpenUsernameGeneratorClick -> {
@@ -562,8 +563,8 @@ class VaultAddEditViewModel @Inject constructor(
handleLoginSetupTotpClick(action)
}
is VaultAddEditAction.ItemType.LoginType.UriSettingsClick -> {
handleLoginUriSettingsClick()
is VaultAddEditAction.ItemType.LoginType.RemoveUriClick -> {
handleLoginRemoveUriClick(action)
}
is VaultAddEditAction.ItemType.LoginType.AddNewUriClick -> {
@@ -596,16 +597,16 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleLoginUriTextInputChange(
action: VaultAddEditAction.ItemType.LoginType.UriTextChange,
private fun handleLoginUriValueInputChange(
action: VaultAddEditAction.ItemType.LoginType.UriValueChange,
) {
updateLoginContent { loginType ->
loginType.copy(
uriList = loginType
.uriList
.map { uriItem ->
if (uriItem.id == action.uri.id) {
action.uri
if (uriItem.id == action.uriItem.id) {
action.uriItem
} else {
uriItem
}
@@ -614,6 +615,18 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleLoginRemoveUriClick(
action: VaultAddEditAction.ItemType.LoginType.RemoveUriClick,
) {
updateLoginContent { loginType ->
loginType.copy(
uriList = loginType.uriList.filter {
it != action.uriItem
},
)
}
}
private fun handleLoginOpenUsernameGeneratorClick() {
sendEvent(event = VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username))
}
@@ -1899,11 +1912,11 @@ sealed class VaultAddEditAction {
data class PasswordTextChange(val password: String) : LoginType()
/**
* Fired when the URI text input is changed.
* Fired when the URI is changed.
*
* @property uri The new URI text.
* @property uriItem The new URI.
*/
data class UriTextChange(val uri: UriItem) : LoginType()
data class UriValueChange(val uriItem: UriItem) : LoginType()
/**
* Represents the action to set up TOTP.
@@ -1940,9 +1953,9 @@ sealed class VaultAddEditAction {
data object OpenPasswordGeneratorClick : LoginType()
/**
* Represents the action of clicking TOTP settings
* Represents the action of removing a URI item.
*/
data object UriSettingsClick : LoginType()
data class RemoveUriClick(val uriItem: UriItem) : LoginType()
/**
* Represents the action to add a new URI field.
@@ -2164,8 +2177,8 @@ sealed class VaultAddEditAction {
* Indicates that the vault item data has been received.
*/
data class VaultDataReceive(
val vaultData: DataState<VaultData>,
val userData: UserState?,
val vaultData: DataState<VaultData>,
val userData: UserState?,
) : Internal()
/**

View File

@@ -10,8 +10,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
*
* @property onUsernameTextChange Handles the action when the username text is changed.
* @property onPasswordTextChange Handles the action when the password text is changed.
* @property onUriTextChange Handles the action when the URI text is changed.
* reprompt toggle is changed.
* @property onRemoveUriClick Handles the action when the URI is removed.
* @property onUriValueChange Handles the action when the URI value is changed.
* @property onOpenUsernameGeneratorClick Handles the action when the username generator
* button is clicked.
* @property onPasswordCheckerClick Handles the action when the password checker
@@ -28,14 +28,14 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
data class VaultAddEditLoginTypeHandlers(
val onUsernameTextChange: (String) -> Unit,
val onPasswordTextChange: (String) -> Unit,
val onUriTextChange: (UriItem) -> Unit,
val onRemoveUriClick: (UriItem) -> Unit,
val onUriValueChange: (UriItem) -> Unit,
val onOpenUsernameGeneratorClick: () -> Unit,
val onPasswordCheckerClick: () -> Unit,
val onOpenPasswordGeneratorClick: () -> Unit,
val onSetupTotpClick: (Boolean) -> Unit,
val onCopyTotpKeyClick: (String) -> Unit,
val onClearTotpKeyClick: () -> Unit,
val onUriSettingsClick: () -> Unit,
val onAddNewUriClick: () -> Unit,
) {
companion object {
@@ -60,9 +60,9 @@ data class VaultAddEditLoginTypeHandlers(
VaultAddEditAction.ItemType.LoginType.PasswordTextChange(newPassword),
)
},
onUriTextChange = { newUri ->
onUriValueChange = { newUri ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.UriTextChange(newUri),
VaultAddEditAction.ItemType.LoginType.UriValueChange(newUri),
)
},
onOpenUsernameGeneratorClick = {
@@ -85,8 +85,12 @@ data class VaultAddEditLoginTypeHandlers(
VaultAddEditAction.ItemType.LoginType.SetupTotpClick(isGranted),
)
},
onUriSettingsClick = {
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.UriSettingsClick)
onRemoveUriClick = { uriItem ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.RemoveUriClick(
uriItem,
),
)
},
onAddNewUriClick = {
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.AddNewUriClick)

View File

@@ -0,0 +1,50 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.model
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* The options displayed to the user when choosing a match type
* for their URI.
*/
@Suppress("MagicNumber")
enum class UriMatchDisplayType(
val text: Text,
) {
/**
* the default option for when the user has not chosen one.
*/
DEFAULT(R.string.default_text.asText()),
/**
* The URIs match if their top-level and second-level domains match.
*/
BASE_DOMAIN(R.string.base_domain.asText()),
/**
* The URIs match if their hostnames (and ports if specified) match.
*/
HOST(R.string.host.asText()),
/**
* The URIs match if the "test" URI starts with the known URI.
*/
STARTS_WITH(R.string.starts_with.asText()),
/**
* The URIs match if the "test" URI matches the known URI according to a specified regular
* expression for the item.
*/
REGULAR_EXPRESSION(R.string.reg_ex.asText()),
/**
* The URIs match if they are exactly the same.
*/
EXACT(R.string.exact.asText()),
/**
* The URIs should never match.
*/
NEVER(R.string.never.asText()),
}

View File

@@ -0,0 +1,32 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
/**
* Method to convert the SDK match type for display to the user.
*/
fun UriMatchType?.toDisplayMatchType(): UriMatchDisplayType =
when (this) {
UriMatchType.DOMAIN -> UriMatchDisplayType.BASE_DOMAIN
UriMatchType.EXACT -> UriMatchDisplayType.EXACT
UriMatchType.HOST -> UriMatchDisplayType.HOST
UriMatchType.NEVER -> UriMatchDisplayType.NEVER
UriMatchType.REGULAR_EXPRESSION -> UriMatchDisplayType.REGULAR_EXPRESSION
UriMatchType.STARTS_WITH -> UriMatchDisplayType.STARTS_WITH
null -> UriMatchDisplayType.DEFAULT
}
/**
* Method to convert the match display type over to the SDK match type.
*/
fun UriMatchDisplayType.toUriMatchType(): UriMatchType? =
when (this) {
UriMatchDisplayType.DEFAULT -> null
UriMatchDisplayType.BASE_DOMAIN -> UriMatchType.DOMAIN
UriMatchDisplayType.HOST -> UriMatchType.HOST
UriMatchDisplayType.STARTS_WITH -> UriMatchType.STARTS_WITH
UriMatchDisplayType.REGULAR_EXPRESSION -> UriMatchType.REGULAR_EXPRESSION
UriMatchDisplayType.EXACT -> UriMatchType.EXACT
UriMatchDisplayType.NEVER -> UriMatchType.NEVER
}