BIT-666: Create UI for Secure Note-type item creation (#319)

This commit is contained in:
Oleg Semenenko
2023-12-05 12:16:26 -06:00
committed by Álison Fernandes
parent 4ce89abbbf
commit c729d7da1b
5 changed files with 996 additions and 37 deletions

View File

@@ -2,10 +2,12 @@ package com.x8bit.bitwarden.ui.vault.feature.additem
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@@ -55,7 +57,6 @@ fun VaultAddItemScreen(
onNavigateBack: () -> Unit,
viewModel: VaultAddItemViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollState = rememberScrollState()
val context = LocalContext.current
@@ -74,10 +75,15 @@ fun VaultAddItemScreen(
VaultAddLoginItemTypeHandlers.create(viewModel = viewModel)
}
val secureNotesTypeHandlers = remember(viewModel) {
VaultAddSecureNotesItemTypeHandlers.create(viewModel = viewModel)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.imePadding()
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
@@ -139,8 +145,11 @@ fun VaultAddItemScreen(
// TODO(BIT-667): Create UI for identity-type item creation
}
VaultAddItemState.ItemType.SecureNotes -> {
// TODO(BIT-666): Create UI for secure notes type item creation
is VaultAddItemState.ItemType.SecureNotes -> {
AddSecureNotesTypeItemContent(
state = selectedType,
secureNotesTypeHandlers = secureNotesTypeHandlers,
)
}
}
}
@@ -173,7 +182,7 @@ private fun TypeOptionsItem(
@Suppress("LongMethod")
@Composable
private fun AddLoginTypeItemContent(
private fun ColumnScope.AddLoginTypeItemContent(
state: VaultAddItemState.ItemType.Login,
loginItemTypeHandlers: VaultAddLoginItemTypeHandlers,
) {
@@ -340,6 +349,7 @@ private fun AddLoginTypeItemContent(
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = state.notes,
onValueChange = loginItemTypeHandlers.onNotesTextChange,
@@ -384,3 +394,125 @@ private fun AddLoginTypeItemContent(
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
@Suppress("LongMethod")
@Composable
private fun ColumnScope.AddSecureNotesTypeItemContent(
state: VaultAddItemState.ItemType.SecureNotes,
secureNotesTypeHandlers: VaultAddSecureNotesItemTypeHandlers,
) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = state.name,
onValueChange = secureNotesTypeHandlers.onNameTextChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.miscellaneous),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.folder),
options = state.availableFolders.map { it.invoke() }.toImmutableList(),
selectedOption = state.folderName.invoke(),
onOptionSelected = secureNotesTypeHandlers.onFolderTextChange,
modifier = Modifier
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(id = R.string.favorite),
isChecked = state.favorite,
onCheckedChange = secureNotesTypeHandlers.onToggleFavorite,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitchWithActions(
label = stringResource(id = R.string.password_prompt),
isChecked = state.masterPasswordReprompt,
onCheckedChange = secureNotesTypeHandlers.onToggleMasterPasswordReprompt,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
actions = {
IconButton(onClick = secureNotesTypeHandlers.onTooltipClick) {
Icon(
painter = painterResource(id = R.drawable.ic_tooltip),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = stringResource(
id = R.string.master_password_re_prompt_help,
),
)
}
},
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = state.notes,
onValueChange = secureNotesTypeHandlers.onNotesTextChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenFilledTonalButton(
label = stringResource(id = R.string.new_custom_field),
onClick = secureNotesTypeHandlers.onAddNewCustomFieldClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ownership),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.who_owns_this_item),
options = state.availableOwners.toImmutableList(),
selectedOption = state.ownership,
onOptionSelected = secureNotesTypeHandlers.onOwnershipTextChange,
modifier = Modifier
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}

View File

@@ -7,9 +7,10 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.additem.VaultAddItemState.ItemType.Card.displayStringResId
import com.x8bit.bitwarden.ui.vault.feature.additem.VaultAddItemState.ItemType.Identity.displayStringResId
import com.x8bit.bitwarden.ui.vault.feature.additem.VaultAddItemState.ItemType.SecureNotes.displayStringResId
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
@@ -63,6 +64,10 @@ class VaultAddItemViewModel @Inject constructor(
handleAddLoginTypeAction(action)
}
is VaultAddItemAction.ItemType.SecureNotesType -> {
handleAddSecureNoteTypeAction(action)
}
is VaultAddItemAction.Internal.CreateCipherResultReceive -> {
handleCreateCipherResultReceive(action)
}
@@ -75,13 +80,29 @@ class VaultAddItemViewModel @Inject constructor(
private fun handleSaveClick() {
viewModelScope.launch {
sendAction(
action = VaultAddItemAction.Internal.CreateCipherResultReceive(
createCipherResult = vaultRepository.createCipher(
cipherView = stateFlow.value.selectedType.toCipherView(),
),
),
)
when (state.selectedType) {
is VaultAddItemState.ItemType.Login -> {
sendAction(
action = VaultAddItemAction.Internal.CreateCipherResultReceive(
createCipherResult = vaultRepository.createCipher(
cipherView = stateFlow.value.selectedType.toCipherView(),
),
),
)
}
is VaultAddItemState.ItemType.SecureNotes -> {
// TODO Add Saving of SecureNotes (BIT-509)
}
VaultAddItemState.ItemType.Card -> {
// TODO Add Saving of SecureNotes (BIT-668)
}
VaultAddItemState.ItemType.Identity -> {
// TODO Add Saving of SecureNotes (BIT-508)
}
}
}
}
@@ -112,6 +133,30 @@ class VaultAddItemViewModel @Inject constructor(
}
}
private fun handleSwitchToAddSecureNotesItem() {
mutableStateFlow.update { currentState ->
currentState.copy(
selectedType = VaultAddItemState.ItemType.SecureNotes(),
)
}
}
private fun handleSwitchToAddCardItem() {
mutableStateFlow.update { currentState ->
currentState.copy(
selectedType = VaultAddItemState.ItemType.Card,
)
}
}
private fun handleSwitchToAddIdentityItem() {
mutableStateFlow.update { currentState ->
currentState.copy(
selectedType = VaultAddItemState.ItemType.Identity,
)
}
}
//endregion Type Option Handlers
//region Add Login Item Type Handlers
@@ -191,30 +236,6 @@ class VaultAddItemViewModel @Inject constructor(
}
}
private fun handleSwitchToAddCardItem() {
mutableStateFlow.update { currentState ->
currentState.copy(
selectedType = VaultAddItemState.ItemType.Card,
)
}
}
private fun handleSwitchToAddIdentityItem() {
mutableStateFlow.update { currentState ->
currentState.copy(
selectedType = VaultAddItemState.ItemType.Identity,
)
}
}
private fun handleSwitchToAddSecureNotesItem() {
mutableStateFlow.update { currentState ->
currentState.copy(
selectedType = VaultAddItemState.ItemType.SecureNotes,
)
}
}
private fun handleNameTextInputChange(
action: VaultAddItemAction.ItemType.LoginType.NameTextChange,
) {
@@ -369,6 +390,114 @@ class VaultAddItemViewModel @Inject constructor(
//endregion Add Login Item Type Handlers
//region Secure Note Item Type Handlers
private fun handleAddSecureNoteTypeAction(
action: VaultAddItemAction.ItemType.SecureNotesType,
) {
when (action) {
is VaultAddItemAction.ItemType.SecureNotesType.NameTextChange -> {
handleSecureNoteNameTextInputChange(action)
}
is VaultAddItemAction.ItemType.SecureNotesType.FolderChange -> {
handleSecureNoteFolderTextInputChange(action)
}
is VaultAddItemAction.ItemType.SecureNotesType.ToggleFavorite -> {
handleSecureNoteToggleFavorite(action)
}
is VaultAddItemAction.ItemType.SecureNotesType.ToggleMasterPasswordReprompt -> {
handleSecureNoteToggleMasterPasswordReprompt(action)
}
is VaultAddItemAction.ItemType.SecureNotesType.NotesTextChange -> {
handleSecureNoteNotesTextInputChange(action)
}
is VaultAddItemAction.ItemType.SecureNotesType.OwnershipChange -> {
handleSecureNoteOwnershipTextInputChange(action)
}
is VaultAddItemAction.ItemType.SecureNotesType.TooltipClick -> {
handleSecureNoteTooltipClick()
}
is VaultAddItemAction.ItemType.SecureNotesType.AddNewCustomFieldClick -> {
handleSecureNoteAddNewCustomFieldClick()
}
}
}
private fun handleSecureNoteNameTextInputChange(
action: VaultAddItemAction.ItemType.SecureNotesType.NameTextChange,
) {
updateSecureNoteType { secureNoteType ->
secureNoteType.copy(name = action.name)
}
}
private fun handleSecureNoteFolderTextInputChange(
action: VaultAddItemAction.ItemType.SecureNotesType.FolderChange,
) {
updateSecureNoteType { secureNoteType ->
secureNoteType.copy(folderName = action.folderName)
}
}
private fun handleSecureNoteToggleFavorite(
action: VaultAddItemAction.ItemType.SecureNotesType.ToggleFavorite,
) {
updateSecureNoteType { secureNoteType ->
secureNoteType.copy(favorite = action.isFavorite)
}
}
private fun handleSecureNoteToggleMasterPasswordReprompt(
action: VaultAddItemAction.ItemType.SecureNotesType.ToggleMasterPasswordReprompt,
) {
updateSecureNoteType { secureNoteType ->
secureNoteType.copy(masterPasswordReprompt = action.isMasterPasswordReprompt)
}
}
private fun handleSecureNoteNotesTextInputChange(
action: VaultAddItemAction.ItemType.SecureNotesType.NotesTextChange,
) {
updateSecureNoteType { secureNoteType ->
secureNoteType.copy(notes = action.note)
}
}
private fun handleSecureNoteOwnershipTextInputChange(
action: VaultAddItemAction.ItemType.SecureNotesType.OwnershipChange,
) {
updateSecureNoteType { secureNoteType ->
secureNoteType.copy(ownership = action.ownership)
}
}
private fun handleSecureNoteTooltipClick() {
// TODO Add the text for the prompt (BIT-1079)
sendEvent(
event = VaultAddItemEvent.ShowToast(
message = "Not yet implemented",
),
)
}
private fun handleSecureNoteAddNewCustomFieldClick() {
// TODO Implement custom text fields (BIT-529)
sendEvent(
event = VaultAddItemEvent.ShowToast(
message = "Not yet implemented",
),
)
}
//endregion Secure Notes Item Type Handlers
//region Internal Type Handlers
@Suppress("MaxLineLength")
@@ -408,6 +537,23 @@ class VaultAddItemViewModel @Inject constructor(
}
}
private inline fun updateSecureNoteType(
crossinline block: (
VaultAddItemState.ItemType.SecureNotes,
) -> VaultAddItemState.ItemType.SecureNotes,
) {
mutableStateFlow.update { currentState ->
val currentSelectedType = currentState.selectedType
if (currentSelectedType !is VaultAddItemState.ItemType.SecureNotes) {
return@update currentState
}
val updatedSecureNote = block(currentSelectedType)
currentState.copy(selectedType = updatedSecureNote)
}
}
//endregion Utility Functions
companion object {
@@ -540,9 +686,28 @@ data class VaultAddItemState(
* @property displayStringResId Resource ID for the display string of the secure notes type.
*/
@Parcelize
data object SecureNotes : ItemType() {
data class SecureNotes(
val name: String = "",
val folderName: Text = DEFAULT_FOLDER,
val favorite: Boolean = false,
val masterPasswordReprompt: Boolean = false,
val notes: String = "",
val ownership: String = DEFAULT_OWNERSHIP,
val availableFolders: List<Text> = listOf(
"Folder 1".asText(),
"Folder 2".asText(),
"Folder 3".asText(),
),
val availableOwners: List<String> = listOf("a@b.com", "c@d.com"),
) : ItemType() {
override val displayStringResId: Int
get() = ItemTypeOption.SECURE_NOTES.labelRes
companion object {
private val DEFAULT_FOLDER: Text = R.string.folder_none.asText()
private const val DEFAULT_OWNERSHIP: String = "placeholder@email.com"
}
}
}
}
@@ -703,6 +868,66 @@ sealed class VaultAddItemAction {
*/
data object AddNewCustomFieldClick : LoginType()
}
/**
* Represents actions specific to the SecureNotes type.
*/
sealed class SecureNotesType : ItemType() {
/**
* Fired when the name text input is changed.
*
* @property name The new name text.
*/
data class NameTextChange(val name: String) : SecureNotesType()
/**
* Fired when the folder text input is changed.
*
* @property folderName The new folder text.
*/
data class FolderChange(val folderName: Text) : SecureNotesType()
/**
* Fired when the Favorite toggle is changed.
*
* @property isFavorite The new state of the Favorite toggle.
*/
data class ToggleFavorite(val isFavorite: Boolean) : SecureNotesType()
/**
* Fired when the Master Password Reprompt toggle is changed.
*
* @property isMasterPasswordReprompt The new state of the Master
* Password Re-prompt toggle.
*/
data class ToggleMasterPasswordReprompt(
val isMasterPasswordReprompt: Boolean,
) : SecureNotesType()
/**
* Fired when the note text input is changed.
*
* @property note The new note text.
*/
data class NotesTextChange(val note: String) : SecureNotesType()
/**
* Fired when the ownership text input is changed.
*
* @property ownership The new ownership text.
*/
data class OwnershipChange(val ownership: String) : SecureNotesType()
/**
* Represents the action to open tooltip
*/
data object TooltipClick : SecureNotesType()
/**
* Represents the action to add a new custom field.
*/
data object AddNewCustomFieldClick : SecureNotesType()
}
}
/**

View File

@@ -0,0 +1,87 @@
package com.x8bit.bitwarden.ui.vault.feature.additem
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* A collection of handler functions specifically tailored for managing actions
* within the context of adding secure note items to a vault.
*
* @property onNameTextChange Handles the action when the name text is changed.
* @property onFolderTextChange Handles the action when the folder text is changed.
* @property onToggleFavorite Handles the action when the favorite toggle is changed.
* @property onToggleMasterPasswordReprompt Handles the action when the master password
* reprompt toggle is changed.
* @property onNotesTextChange Handles the action when the notes text is changed.
* @property onOwnershipTextChange Handles the action when the ownership text is changed.
* @property onTooltipClick Handles the action when the tooltip button is clicked.
* @property onAddNewCustomFieldClick Handles the action when the add new custom field
* button is clicked.
*/
@Suppress("LongParameterList")
class VaultAddSecureNotesItemTypeHandlers(
val onNameTextChange: (String) -> Unit,
val onFolderTextChange: (String) -> Unit,
val onToggleFavorite: (Boolean) -> Unit,
val onToggleMasterPasswordReprompt: (Boolean) -> Unit,
val onNotesTextChange: (String) -> Unit,
val onOwnershipTextChange: (String) -> Unit,
val onTooltipClick: () -> Unit,
val onAddNewCustomFieldClick: () -> Unit,
) {
companion object {
/**
* Creates an instance of [VaultAddSecureNotesItemTypeHandlers] by binding actions
* to the provided [VaultAddItemViewModel].
*/
@Suppress("LongMethod")
fun create(viewModel: VaultAddItemViewModel): VaultAddSecureNotesItemTypeHandlers {
return VaultAddSecureNotesItemTypeHandlers(
onNameTextChange = { newName ->
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.NameTextChange(newName),
)
},
onFolderTextChange = { newFolder ->
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.FolderChange(
newFolder.asText(),
),
)
},
onToggleFavorite = { isFavorite ->
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.ToggleFavorite(isFavorite),
)
},
onToggleMasterPasswordReprompt = { isMasterPasswordReprompt ->
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.ToggleMasterPasswordReprompt(
isMasterPasswordReprompt,
),
)
},
onNotesTextChange = { newNotes ->
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.NotesTextChange(newNotes),
)
},
onOwnershipTextChange = { newOwnership ->
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.OwnershipChange(newOwnership),
)
},
onTooltipClick = {
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.TooltipClick,
)
},
onAddNewCustomFieldClick = {
viewModel.trySendAction(
VaultAddItemAction.ItemType.SecureNotesType.AddNewCustomFieldClick,
)
},
)
}
}
}