BIT-634: Create Password Generation UI (#109)

This commit is contained in:
joshua-livefront
2023-10-12 14:41:58 -04:00
committed by Álison Fernandes
parent 2cda9db9a2
commit 9879e6fd23
4 changed files with 1229 additions and 78 deletions

View File

@@ -12,11 +12,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
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.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@@ -29,6 +34,8 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -42,6 +49,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithTwoIcons
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
/**
* Top level composable for the generator screen.
@@ -84,7 +93,90 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
}
}
val onNumWordsCounterChange: (Int) -> Unit = remember(viewModel) {
val onPasswordSliderLengthChange: (Int) -> Unit = remember(viewModel) {
{ newLength ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
length = newLength,
),
)
}
}
val onPasswordToggleCapitalLettersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseCapitals ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
useCapitals = shouldUseCapitals,
),
)
}
}
val onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseLowercase ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleLowercaseLettersChange(
useLowercase = shouldUseLowercase,
),
)
}
}
val onPasswordToggleNumbersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseNumbers ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
useNumbers = shouldUseNumbers,
),
)
}
}
val onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseSpecialChars ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange(
useSpecialChars = shouldUseSpecialChars,
),
)
}
}
val onPasswordMinNumbersCounterChange: (Int) -> Unit = remember(viewModel) {
{ newMinNumbers ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
minNumbers = newMinNumbers,
),
)
}
}
val onPasswordMinSpecialCharactersChange: (Int) -> Unit = remember(viewModel) {
{ newMinSpecial ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
minSpecial = newMinSpecial,
),
)
}
}
val onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldAvoidAmbiguousChars ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = shouldAvoidAmbiguousChars,
),
)
}
}
val onPassphraseNumWordsCounterChange: (Int) -> Unit = remember(viewModel) {
{ changeInCounter ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange(
@@ -104,7 +196,7 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
}
}
val onIncludeNumberToggleChange: (Boolean) -> Unit = remember(viewModel) {
val onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldIncludeNumber ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange(
@@ -114,7 +206,7 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
}
}
val onWordSeparatorChange: (Char?) -> Unit = remember(viewModel) {
val onPassphraseWordSeparatorChange: (Char?) -> Unit = remember(viewModel) {
{ newSeparator ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.WordSeparatorTextChange(
@@ -137,16 +229,29 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
ScrollContent(
state,
onRegenerateClick,
onCopyClick,
onMainStateOptionClicked,
onPasscodeOptionClicked,
onNumWordsCounterChange,
onWordSeparatorChange,
onPassphraseCapitalizeToggleChange,
onIncludeNumberToggleChange,
Modifier.padding(innerPadding),
state = state,
onRegenerateClick = onRegenerateClick,
onCopyClick = onCopyClick,
onMainStateOptionClicked = onMainStateOptionClicked,
onSubStateOptionClicked = onPasscodeOptionClicked,
// Password handlers
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
onPasswordToggleSpecialCharactersChange = onPasswordToggleSpecialCharactersChange,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
onPasswordToggleAvoidAmbiguousCharsChange = onPasswordToggleAvoidAmbiguousCharsChange,
// Passphrase handlers
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
modifier = Modifier.padding(innerPadding),
)
}
}
@@ -161,10 +266,18 @@ private fun ScrollContent(
onCopyClick: () -> Unit,
onMainStateOptionClicked: (GeneratorState.MainTypeOption) -> Unit,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
onNumWordsCounterChange: (Int) -> Unit,
onWordSeparatorChange: (Char?) -> Unit,
onCapitalizeToggleChange: (Boolean) -> Unit,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPasswordSliderLengthChange: (Int) -> Unit,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
onPassphraseWordSeparatorChange: (Char?) -> Unit,
onPassphraseCapitalizeToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -202,12 +315,26 @@ private fun ScrollContent(
when (val selectedType = state.selectedType) {
is GeneratorState.MainType.Passcode -> {
PasscodeTypeItems(
selectedType,
onSubStateOptionClicked,
onNumWordsCounterChange,
onWordSeparatorChange,
onCapitalizeToggleChange,
onIncludeNumberToggleChange,
passcodeState = selectedType,
onSubStateOptionClicked = onSubStateOptionClicked,
// Password handlers
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
onPasswordToggleSpecialCharactersChange =
onPasswordToggleSpecialCharactersChange,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
onPasswordToggleAvoidAmbiguousCharsChange =
onPasswordToggleAvoidAmbiguousCharsChange,
// Passphrase handlers
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
)
}
@@ -265,49 +392,49 @@ private fun MainStateOptionsItem(
//region PasscodeType Composables
/**
* A composable function to represent a collection of passcode type items based on the selected
* [GeneratorState.MainType.Passcode.PasscodeType]. It dynamically displays content depending on
* the currently selected passcode type.
*
* @param passcodeState The current state of the passcode generator,
* holding the selected passcode type and other settings.
* @param onSubStateOptionClicked A lambda function invoked when a substate option is clicked.
* It takes the selected [GeneratorState.MainType.Passcode.PasscodeTypeOption] as a parameter.
* @param onNumWordsCounterChange A lambda function invoked when there is a change
* in the number of words for passphrase. It takes the updated number of words as a parameter.
* @param onWordSeparatorChange A lambda function invoked when there is a change
* in the word separator character for passphrase. It takes the updated character as a parameter,
* `null` if there is no separator.
* @param onCapitalizeToggleChange A lambda function invoked when the capitalize
* toggle state changes for passphrase. It takes the updated toggle state as a parameter.
* @param onIncludeNumberToggleChange A lambda function invoked when the include number toggle
* state changes for passphrase. It takes the updated toggle state as a parameter.
*/
@Composable
fun PasscodeTypeItems(
private fun PasscodeTypeItems(
passcodeState: GeneratorState.MainType.Passcode,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
onNumWordsCounterChange: (Int) -> Unit,
onWordSeparatorChange: (Char?) -> Unit,
onCapitalizeToggleChange: (Boolean) -> Unit,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPasswordSliderLengthChange: (Int) -> Unit,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
onPassphraseWordSeparatorChange: (Char?) -> Unit,
onPassphraseCapitalizeToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
) {
PasscodeOptionsItem(passcodeState, onSubStateOptionClicked)
when (val selectedType = passcodeState.selectedType) {
is GeneratorState.MainType.Passcode.PasscodeType.Passphrase -> {
PassphraseTypeContent(
selectedType,
onNumWordsCounterChange,
onWordSeparatorChange,
onCapitalizeToggleChange,
onIncludeNumberToggleChange,
is GeneratorState.MainType.Passcode.PasscodeType.Password -> {
PasswordTypeContent(
passwordTypeState = selectedType,
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
onPasswordToggleSpecialCharactersChange = onPasswordToggleSpecialCharactersChange,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
onPasswordToggleAvoidAmbiguousCharsChange =
onPasswordToggleAvoidAmbiguousCharsChange,
)
}
is GeneratorState.MainType.Passcode.PasscodeType.Password -> {
// TODO(BIT-334): Render UI for Password type
is GeneratorState.MainType.Passcode.PasscodeType.Passphrase -> {
PassphraseTypeContent(
passphraseTypeState = selectedType,
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
)
}
}
}
@@ -335,34 +462,238 @@ private fun PasscodeOptionsItem(
//endregion PasscodeType Composables
//region PasswordType Composables
@Composable
private fun PasswordTypeContent(
passwordTypeState: GeneratorState.MainType.Passcode.PasscodeType.Password,
onPasswordSliderLengthChange: (Int) -> Unit,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
) {
PasswordLengthSliderItem(
length = passwordTypeState.length,
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
)
Column(
modifier = Modifier.fillMaxWidth(),
) {
PasswordCapitalLettersToggleItem(
useCapitals = passwordTypeState.useCapitals,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
)
PasswordLowercaseLettersToggleItem(
useLowercase = passwordTypeState.useLowercase,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
)
PasswordNumbersToggleItem(
useNumbers = passwordTypeState.useNumbers,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
)
PasswordSpecialCharactersToggleItem(
useSpecialChars = passwordTypeState.useSpecialChars,
onPasswordToggleSpecialCharactersChange = onPasswordToggleSpecialCharactersChange,
)
}
PasswordMinNumbersCounterItem(
minNumbers = passwordTypeState.minNumbers,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
)
PasswordMinSpecialCharactersCounterItem(
minSpecial = passwordTypeState.minSpecial,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
)
PasswordAvoidAmbiguousCharsToggleItem(
avoidAmbiguousChars = passwordTypeState.avoidAmbiguousChars,
onPasswordToggleAvoidAmbiguousCharsChange = onPasswordToggleAvoidAmbiguousCharsChange,
)
}
@Composable
private fun PasswordLengthSliderItem(
length: Int,
onPasswordSliderLengthChange: (Int) -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.semantics(mergeDescendants = true) {},
) {
OutlinedTextField(
value = length.toString(),
readOnly = true,
onValueChange = { newText ->
newText.toIntOrNull()?.let { newValue ->
onPasswordSliderLengthChange(newValue)
}
},
label = { Text(stringResource(id = R.string.length)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier
.wrapContentWidth()
.widthIn(max = 71.dp),
)
Slider(
value = length.toFloat(),
onValueChange = { newValue ->
onPasswordSliderLengthChange(newValue.toInt())
},
valueRange =
PASSWORD_LENGTH_SLIDER_MIN.toFloat()..PASSWORD_LENGTH_SLIDER_MAX.toFloat(),
steps = PASSWORD_LENGTH_SLIDER_MAX - 1,
)
}
}
@Composable
private fun PasswordCapitalLettersToggleItem(
useCapitals: Boolean,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.uppercase_ato_z),
isChecked = useCapitals,
onCheckedChange = onPasswordToggleCapitalLettersChange,
)
}
@Composable
private fun PasswordLowercaseLettersToggleItem(
useLowercase: Boolean,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.lowercase_ato_z),
isChecked = useLowercase,
onCheckedChange = onPasswordToggleLowercaseLettersChange,
)
}
@Composable
private fun PasswordNumbersToggleItem(
useNumbers: Boolean,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.numbers_zero_to_nine),
isChecked = useNumbers,
onCheckedChange = onPasswordToggleNumbersChange,
)
}
@Composable
private fun PasswordSpecialCharactersToggleItem(
useSpecialChars: Boolean,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.special_characters),
isChecked = useSpecialChars,
onCheckedChange = onPasswordToggleSpecialCharactersChange,
)
}
@Composable
private fun PasswordMinNumbersCounterItem(
minNumbers: Int,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
) {
BitwardenTextFieldWithTwoIcons(
label = stringResource(id = R.string.min_numbers),
value = minNumbers.toString(),
firstIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onFirstIconClick = {
onPasswordMinNumbersCounterChange(minNumbers - 1)
},
secondIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onSecondIconClick = {
onPasswordMinNumbersCounterChange(minNumbers + 1)
},
)
}
@Composable
private fun PasswordMinSpecialCharactersCounterItem(
minSpecial: Int,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
) {
BitwardenTextFieldWithTwoIcons(
label = stringResource(id = R.string.min_special),
value = minSpecial.toString(),
firstIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onFirstIconClick = {
onPasswordMinSpecialCharactersChange(minSpecial - 1)
},
secondIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onSecondIconClick = {
onPasswordMinSpecialCharactersChange(minSpecial + 1)
},
)
}
@Composable
private fun PasswordAvoidAmbiguousCharsToggleItem(
avoidAmbiguousChars: Boolean,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.avoid_ambiguous_characters),
isChecked = avoidAmbiguousChars,
onCheckedChange = onPasswordToggleAvoidAmbiguousCharsChange,
)
}
//endregion PasswordType Composables
//region PassphraseType Composables
@Composable
private fun PassphraseTypeContent(
passphraseTypeState: GeneratorState.MainType.Passcode.PasscodeType.Passphrase,
onNumWordsCounterChange: (Int) -> Unit,
onWordSeparatorChange: (Char?) -> Unit,
onCapitalizeToggleChange: (Boolean) -> Unit,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
onPassphraseWordSeparatorChange: (Char?) -> Unit,
onPassphraseCapitalizeToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
) {
PassphraseNumWordsCounterItem(
numWords = passphraseTypeState.numWords,
onNumWordsCounterChange = onNumWordsCounterChange,
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
)
PassphraseWordSeparatorInputItem(
wordSeparator = passphraseTypeState.wordSeparator,
onWordSeparatorChange = onWordSeparatorChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
)
Column(
modifier = Modifier.fillMaxWidth(),
) {
PassphraseCapitalizeToggleItem(
capitalize = passphraseTypeState.capitalize,
onPassphraseCapitalizeToggleChange = onCapitalizeToggleChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
)
PassphraseIncludeNumberToggleItem(
includeNumber = passphraseTypeState.includeNumber,
onIncludeNumberToggleChange = onIncludeNumberToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
)
}
}
@@ -370,7 +701,7 @@ private fun PassphraseTypeContent(
@Composable
private fun PassphraseNumWordsCounterItem(
numWords: Int,
onNumWordsCounterChange: (Int) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
) {
BitwardenTextFieldWithTwoIcons(
label = stringResource(id = R.string.number_of_words),
@@ -380,14 +711,14 @@ private fun PassphraseNumWordsCounterItem(
contentDescription = "\u2212",
),
onFirstIconClick = {
onNumWordsCounterChange(numWords - 1)
onPassphraseNumWordsCounterChange(numWords - 1)
},
secondIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onSecondIconClick = {
onNumWordsCounterChange(numWords + 1)
onPassphraseNumWordsCounterChange(numWords + 1)
},
)
}
@@ -395,13 +726,13 @@ private fun PassphraseNumWordsCounterItem(
@Composable
private fun PassphraseWordSeparatorInputItem(
wordSeparator: Char?,
onWordSeparatorChange: (wordSeparator: Char?) -> Unit,
onPassphraseWordSeparatorChange: (wordSeparator: Char?) -> Unit,
) {
BitwardenTextField(
label = stringResource(id = R.string.word_separator),
value = wordSeparator?.toString() ?: "",
onValueChange = {
onWordSeparatorChange(it.toCharArray().firstOrNull())
onPassphraseWordSeparatorChange(it.toCharArray().firstOrNull())
},
modifier = Modifier.width(267.dp),
)
@@ -422,12 +753,12 @@ private fun PassphraseCapitalizeToggleItem(
@Composable
private fun PassphraseIncludeNumberToggleItem(
includeNumber: Boolean,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.include_number),
isChecked = includeNumber,
onCheckedChange = onIncludeNumberToggleChange,
onCheckedChange = onPassphraseIncludeNumberToggleChange,
)
}

View File

@@ -12,6 +12,8 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Pa
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MAX_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MIN_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MAX
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeTypeOption
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
@@ -67,6 +69,10 @@ class GeneratorViewModel @Inject constructor(
handlePasscodeTypeOptionSelect(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password -> {
handlePasswordSpecificAction(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase -> {
handlePassphraseSpecificAction(action)
}
@@ -158,6 +164,139 @@ class GeneratorViewModel @Inject constructor(
//endregion Passcode Type Handlers
//region Password Specific Handlers
private fun handlePasswordSpecificAction(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password,
) {
when (action) {
is GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange,
-> {
handlePasswordLengthSliderChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange,
-> {
handleToggleCapitalLetters(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleLowercaseLettersChange,
-> {
handleToggleLowercaseLetters(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange,
-> {
handleToggleNumbers(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange,
-> {
handleToggleSpecialChars(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
-> {
handleMinNumbersChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
-> {
handleMinSpecialChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange,
-> {
handleToggleAmbiguousChars(action)
}
}
}
private fun handlePasswordLengthSliderChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange,
) {
val adjustedLength = action.length
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(length = adjustedLength)
}
}
private fun handleToggleCapitalLetters(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
useCapitals = action.useCapitals,
)
}
}
private fun handleToggleLowercaseLetters(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleLowercaseLettersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(useLowercase = action.useLowercase)
}
}
private fun handleToggleNumbers(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(useNumbers = action.useNumbers)
}
}
private fun handleToggleSpecialChars(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(useSpecialChars = action.useSpecialChars)
}
}
private fun handleMinNumbersChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
) {
val adjustedMinNumbers = action
.minNumbers
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(minNumbers = adjustedMinNumbers)
}
}
private fun handleMinSpecialChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
) {
val adjustedMinSpecial = action
.minSpecial
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(minSpecial = adjustedMinSpecial)
}
}
private fun handleToggleAmbiguousChars(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
avoidAmbiguousChars = action.avoidAmbiguousChars,
)
}
}
//endregion Password Specific Handlers
//region Passphrase Specific Handlers
private fun handlePassphraseSpecificAction(
@@ -248,6 +387,18 @@ class GeneratorViewModel @Inject constructor(
}
}
private inline fun updatePasswordType(
crossinline block: (Password) -> Password,
) {
updateGeneratorMainTypePassword { currentSelectedType ->
val currentPasswordType = currentSelectedType.selectedType
if (currentPasswordType !is Password) {
return@updateGeneratorMainTypePassword currentSelectedType
}
currentSelectedType.copy(selectedType = block(currentPasswordType))
}
}
private inline fun updatePassphraseType(
crossinline block: (Passphrase) -> Passphrase,
) {
@@ -268,8 +419,7 @@ class GeneratorViewModel @Inject constructor(
val INITIAL_STATE: GeneratorState = GeneratorState(
generatedText = PLACEHOLDER_GENERATED_TEXT,
selectedType = Passcode(
// TODO (BIT-634): Update the initial state to Password
selectedType = Passphrase(),
selectedType = Password(),
),
)
}
@@ -358,19 +508,41 @@ data class GeneratorState(
abstract val displayStringResId: Int
/**
* Represents a standard PASSWORD type, with a specified length.
* Represents a standard PASSWORD type, with configurable options for
* length, character types, and requirements.
*
* @property length The length of the generated password.
* @property useCapitals Whether to include capital letters.
* @property useLowercase Whether to include lowercase letters.
* @property useNumbers Whether to include numbers.
* @property useSpecialChars Whether to include special characters.
* @property minNumbers The minimum number of numeric characters.
* @property minSpecial The minimum number of special characters.
* @property avoidAmbiguousChars Whether to avoid characters that look similar.
*/
@Parcelize
data class Password(
val length: Int = DEFAULT_PASSWORD_LENGTH,
val useCapitals: Boolean = true,
val useLowercase: Boolean = true,
val useNumbers: Boolean = true,
val useSpecialChars: Boolean = false,
val minNumbers: Int = MIN_NUMBERS,
val minSpecial: Int = MIN_SPECIAL,
val avoidAmbiguousChars: Boolean = false,
) : PasscodeType(), Parcelable {
override val displayStringResId: Int
get() = PasscodeTypeOption.PASSWORD.labelRes
companion object {
const val DEFAULT_PASSWORD_LENGTH: Int = 10
private const val DEFAULT_PASSWORD_LENGTH: Int = 14
private const val MIN_NUMBERS: Int = 1
private const val MIN_SPECIAL: Int = 1
const val PASSWORD_LENGTH_SLIDER_MIN: Int = 5
const val PASSWORD_LENGTH_SLIDER_MAX: Int = 128
const val PASSWORD_COUNTER_MIN: Int = 0
const val PASSWORD_COUNTER_MAX: Int = 5
}
}
@@ -470,7 +642,94 @@ sealed class GeneratorAction {
/**
* Represents actions specifically related to passwords, a subtype of passcode.
*/
sealed class Password : PasscodeType()
sealed class Password : PasscodeType() {
/**
* Represents a change action for the length of the password,
* adjusted using a slider.
*
* @property length The new desired length for the password.
*/
data class SliderLengthChange(
val length: Int,
) : Password()
/**
* Represents a change action to toggle the usage of
* capital letters in the password.
*
* @property useCapitals Flag indicating whether capital letters
* should be used.
*/
data class ToggleCapitalLettersChange(
val useCapitals: Boolean,
) : Password()
/**
* Represents a change action to toggle the usage of lowercase letters
* in the password.
*
* @property useLowercase Flag indicating whether lowercase letters
* should be used.
*/
data class ToggleLowercaseLettersChange(
val useLowercase: Boolean,
) : Password()
/**
* Represents a change action to toggle the inclusion of numbers
* in the password.
*
* @property useNumbers Flag indicating whether numbers
* should be used.
*/
data class ToggleNumbersChange(
val useNumbers: Boolean,
) : Password()
/**
* Represents a change action to toggle the usage of special characters
* in the password.
*
* @property useSpecialChars Flag indicating whether special characters
* should be used.
*/
data class ToggleSpecialCharactersChange(
val useSpecialChars: Boolean,
) : Password()
/**
* Represents a change action for the minimum required number of numbers
* in the password.
*
* @property minNumbers The minimum required number of numbers
* for the password.
*/
data class MinNumbersCounterChange(
val minNumbers: Int,
) : Password()
/**
* Represents a change action for the minimum required number of special
* characters in the password.
*
* @property minSpecial The minimum required number of special characters
* for the password.
*/
data class MinSpecialCharactersChange(
val minSpecial: Int,
) : Password()
/**
* Represents a change action to toggle the avoidance of ambiguous
* characters in the password.
*
* @property avoidAmbiguousChars Flag indicating whether ambiguous characters
* should be avoided.
*/
data class ToggleAvoidAmbigousCharactersChange(
val avoidAmbiguousChars: Boolean,
) : Password()
}
/**
* Represents actions specifically related to passphrases, a subtype of passcode.