BIT-659: Adding UI for plus addressed email generator (#293)

This commit is contained in:
joshua-livefront
2024-06-20 17:08:07 +01:00
committed by Álison Fernandes
parent 8e099a546d
commit 90f565e02b
4 changed files with 507 additions and 20 deletions
@@ -15,6 +15,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.SnackbarDuration
@@ -121,10 +122,23 @@ fun GeneratorScreen(
}
}
val onUsernameOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit =
remember(viewModel) {
{
viewModel.trySendAction(
GeneratorAction.MainType.Username.UsernameTypeOptionSelect(
it,
),
)
}
}
val passwordHandlers = PasswordHandlers.create(viewModel = viewModel)
val passphraseHandlers = PassphraseHandlers.create(viewModel = viewModel)
val plusAddressedEmailHandlers = PlusAddressedEmailHandlers.create(viewModel = viewModel)
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
@@ -148,9 +162,11 @@ fun GeneratorScreen(
onRegenerateClick = onRegenerateClick,
onCopyClick = onCopyClick,
onMainStateOptionClicked = onMainStateOptionClicked,
onSubStateOptionClicked = onPasscodeOptionClicked,
onPasscodeSubStateOptionClicked = onPasscodeOptionClicked,
onUsernameSubStateOptionClicked = onUsernameOptionClicked,
passwordHandlers = passwordHandlers,
passphraseHandlers = passphraseHandlers,
plusAddressedEmailHandlers = plusAddressedEmailHandlers,
modifier = Modifier.padding(innerPadding),
)
}
@@ -165,9 +181,11 @@ private fun ScrollContent(
onRegenerateClick: () -> Unit,
onCopyClick: () -> Unit,
onMainStateOptionClicked: (GeneratorState.MainTypeOption) -> Unit,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
onPasscodeSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
onUsernameSubStateOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit,
passwordHandlers: PasswordHandlers,
passphraseHandlers: PassphraseHandlers,
plusAddressedEmailHandlers: PlusAddressedEmailHandlers,
modifier: Modifier = Modifier,
) {
Column(
@@ -205,14 +223,18 @@ private fun ScrollContent(
is GeneratorState.MainType.Passcode -> {
PasscodeTypeItems(
passcodeState = selectedType,
onSubStateOptionClicked = onSubStateOptionClicked,
onSubStateOptionClicked = onPasscodeSubStateOptionClicked,
passwordHandlers = passwordHandlers,
passphraseHandlers = passphraseHandlers,
)
}
is GeneratorState.MainType.Username -> {
// TODO(BIT-335): Username state to handle Plus Addressed Email
UsernameTypeItems(
usernameState = selectedType,
onSubStateOptionClicked = onUsernameSubStateOptionClicked,
plusAddressedEmailHandlers = plusAddressedEmailHandlers,
)
}
}
}
@@ -661,6 +683,109 @@ private fun PassphraseIncludeNumberToggleItem(
//endregion PassphraseType Composables
//region UsernameType Composables
@Composable
private fun UsernameTypeItems(
usernameState: GeneratorState.MainType.Username,
onSubStateOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit,
plusAddressedEmailHandlers: PlusAddressedEmailHandlers,
) {
UsernameOptionsItem(usernameState, onSubStateOptionClicked)
when (val selectedType = usernameState.selectedType) {
is GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail -> {
PlusAddressedEmailTypeContent(
usernameTypeState = selectedType,
plusAddressedEmailHandlers = plusAddressedEmailHandlers,
)
}
is GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias -> {
// TODO: Implement ForwardedEmailAlias BIT-657
}
is GeneratorState.MainType.Username.UsernameType.CatchAllEmail -> {
// TODO: Implement CatchAllEmail BIT-656
}
is GeneratorState.MainType.Username.UsernameType.RandomWord -> {
// TODO: Implement RandomWord BIT-658
}
}
}
@Composable
private fun UsernameOptionsItem(
currentSubState: GeneratorState.MainType.Username,
onSubStateOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit,
) {
val possibleSubStates = GeneratorState.MainType.Username.UsernameTypeOption.values().toList()
val optionsWithStrings =
possibleSubStates.associateBy({ it }, { stringResource(id = it.labelRes) })
BitwardenMultiSelectButton(
label = stringResource(id = R.string.username_type),
options = optionsWithStrings.values.toList(),
selectedOption = stringResource(id = currentSubState.selectedType.displayStringResId),
onOptionSelected = { selectedOption ->
val selectedOptionId =
optionsWithStrings.entries.first { it.value == selectedOption }.key
onSubStateOptionClicked(selectedOptionId)
},
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
//endregion UsernameType Composables
//region PlusAddressedEmailType Composables
@Composable
private fun PlusAddressedEmailTypeContent(
usernameTypeState: GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail,
plusAddressedEmailHandlers: PlusAddressedEmailHandlers,
) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.plus_addressed_email_description),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(8.dp))
PlusAddressedEmailTextInputItem(
email = usernameTypeState.email,
onPlusAddressedEmailTextChange = plusAddressedEmailHandlers.onEmailChange,
)
}
@Composable
private fun PlusAddressedEmailTextInputItem(
email: String,
onPlusAddressedEmailTextChange: (email: String) -> Unit,
) {
BitwardenTextField(
label = stringResource(id = R.string.email_required_parenthesis),
value = email,
onValueChange = {
onPlusAddressedEmailTextChange(it)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
//endregion PlusAddressedEmailType Composables
@Preview(showBackground = true)
@Composable
private fun GeneratorPreview() {
@@ -808,3 +933,32 @@ private class PassphraseHandlers(
}
}
}
/**
* A class dedicated to handling user interactions related to plus addressed email
* configuration.
* Each lambda corresponds to a specific user action, allowing for easy delegation of
* logic when user input is detected.
*/
private class PlusAddressedEmailHandlers(
val onEmailChange: (String) -> Unit,
) {
companion object {
fun create(viewModel: GeneratorViewModel): PlusAddressedEmailHandlers {
return PlusAddressedEmailHandlers(
onEmailChange = { newEmail ->
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.PlusAddressedEmail
.EmailTextChange(
email = newEmail,
),
)
},
)
}
}
}
@@ -95,6 +95,15 @@ class GeneratorViewModel @Inject constructor(
is GeneratorAction.Internal.UpdateGeneratedPassphraseResult -> {
handleUpdateGeneratedPassphraseResult(action)
}
is GeneratorAction.MainType.Username.UsernameTypeOptionSelect -> {
handleUsernameTypeOptionSelect(action)
}
is GeneratorAction.MainType.Username.UsernameType.PlusAddressedEmail.EmailTextChange ->
{
handlePlusAddressedEmailTextInputChange(action)
}
}
}
@@ -492,6 +501,45 @@ class GeneratorViewModel @Inject constructor(
//endregion Passphrase Specific Handlers
//region Username Type Handlers
private fun handleUsernameTypeOptionSelect(
action: GeneratorAction.MainType.Username.UsernameTypeOptionSelect,
) {
when (action.usernameTypeOption) {
Username.UsernameTypeOption.PLUS_ADDRESSED_EMAIL -> loadUsernameOptions(
selectedType = Username(selectedType = PlusAddressedEmail()),
)
Username.UsernameTypeOption.CATCH_ALL_EMAIL -> loadUsernameOptions(
selectedType = Username(selectedType = Username.UsernameType.CatchAllEmail()),
)
Username.UsernameTypeOption.FORWARDED_EMAIL_ALIAS -> loadUsernameOptions(
selectedType = Username(selectedType = Username.UsernameType.ForwardedEmailAlias()),
)
Username.UsernameTypeOption.RANDOM_WORD -> loadUsernameOptions(
selectedType = Username(selectedType = Username.UsernameType.RandomWord()),
)
}
}
//endregion Username Type Handlers
//region Plus Addressed Email Specific Handlers
private fun handlePlusAddressedEmailTextInputChange(
action: GeneratorAction.MainType.Username.UsernameType.PlusAddressedEmail.EmailTextChange,
) {
updatePlusAddressedEmailType { plusAddressedEmailType ->
val newEmail = action.email
plusAddressedEmailType.copy(email = newEmail)
}
}
//endregion Plus Addressed Email Specific Handlers
//region Utility Functions
private inline fun updateGeneratorMainType(
@@ -555,6 +603,26 @@ class GeneratorViewModel @Inject constructor(
}
}
private inline fun updateGeneratorMainTypeUsername(
crossinline block: (Username) -> Username,
) {
updateGeneratorMainType {
if (it !is Username) null else block(it)
}
}
private inline fun updatePlusAddressedEmailType(
crossinline block: (PlusAddressedEmail) -> PlusAddressedEmail,
) {
updateGeneratorMainTypeUsername { currentSelectedType ->
val currentUsernameType = currentSelectedType.selectedType
if (currentUsernameType !is PlusAddressedEmail) {
return@updateGeneratorMainTypeUsername currentSelectedType
}
currentSelectedType.copy(selectedType = block(currentUsernameType))
}
}
//endregion Utility Functions
companion object {
@@ -1107,7 +1175,36 @@ sealed class GeneratorAction {
* This sealed class serves as a placeholder for future extensions
* related to the username actions in the generator.
*/
sealed class Username : MainType()
sealed class Username : MainType() {
/**
* Represents the action of selecting a username type option.
*
* @property usernameTypeOption The selected username type option.
*/
data class UsernameTypeOptionSelect(
val usernameTypeOption: GeneratorState.MainType.Username.UsernameTypeOption,
) : Username()
/**
* Represents actions related to the different types of usernames.
*/
sealed class UsernameType : Username() {
/**
* Represents actions specifically related to Plus Addressed Email.
*/
sealed class PlusAddressedEmail : UsernameType() {
/**
* Fired when the email text input is changed.
*
* @property email The new email text.
*/
data class EmailTextChange(val email: String) : PlusAddressedEmail()
}
}
}
}
/**