mirror of
https://github.com/bitwarden/android.git
synced 2026-08-01 18:53:39 -05:00
BIT-659: Adding UI for plus addressed email generator (#293)
This commit is contained in:
committed by
Álison Fernandes
parent
8e099a546d
commit
90f565e02b
+158
-4
@@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+98
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user