mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 11:59:57 +03:00
BIT-634: Create Password Generation UI (#109)
This commit is contained in:
parent
2cda9db9a2
commit
9879e6fd23
4 changed files with 1229 additions and 78 deletions
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -2,19 +2,29 @@
|
|||
|
||||
package com.x8bit.bitwarden.ui.tools.feature.generator
|
||||
|
||||
import androidx.compose.ui.semantics.ProgressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.Role.Companion.Switch
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.SemanticsProperties.Role
|
||||
import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasProgressBarRangeInfo
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onChildren
|
||||
import androidx.compose.ui.test.onLast
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onSiblings
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeRight
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -79,6 +89,333 @@ class GeneratorScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
//region Passcode Password Tests
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, the ViewModel state should update the UI correctly`() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "Password, Password")
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNode(
|
||||
expectValue(
|
||||
SemanticsProperties.EditableText, AnnotatedString("14"),
|
||||
),
|
||||
)
|
||||
.assertExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Uppercase (A to Z)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.assertIsOn()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Lowercase (A to Z)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.assertIsOn()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Numbers (0 to 9)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.assertIsOn()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Special characters (!@#$%^*)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.assertIsOff()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Minimum numbers, 1")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("\u2212"))
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Minimum numbers, 1")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("+"))
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Avoid ambiguous characters")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.performScrollTo()
|
||||
.assertIsOff()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, adjusting the slider should send SliderLengthChange action with length not equal to default`() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Length")
|
||||
.onSiblings()
|
||||
.filterToOne(
|
||||
hasProgressBarRangeInfo(
|
||||
ProgressBarRangeInfo(
|
||||
current = 13.6484375f,
|
||||
range = 5.0f..128.0f,
|
||||
steps = 127,
|
||||
),
|
||||
),
|
||||
)
|
||||
.performScrollTo()
|
||||
.performTouchInput {
|
||||
swipeRight(50f, 800f)
|
||||
}
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
|
||||
length = 128,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, toggling the capital letters toggle should send ToggleCapitalLettersChange action`() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Uppercase (A to Z)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
|
||||
useCapitals = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, toggling the use lowercase toggle should send ToggleLowercaseLettersChange action`() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Lowercase (A to Z)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleLowercaseLettersChange(
|
||||
useLowercase = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, toggling the use numbers toggle should send ToggleNumbersChange action`() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Numbers (0 to 9)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
|
||||
useNumbers = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, toggling the use special characters toggle should send ToggleSpecialCharactersChange action`() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Special characters (!@#$%^*)")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleSpecialCharactersChange(
|
||||
useSpecialChars = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, decrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
|
||||
val initialMinNumbers = 1
|
||||
updateState(
|
||||
GeneratorState(
|
||||
generatedText = "Placeholder",
|
||||
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("\u2212"))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
|
||||
minNumbers = initialMinNumbers - 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, incrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
|
||||
val initialMinNumbers = 1
|
||||
updateState(
|
||||
GeneratorState(
|
||||
generatedText = "Placeholder",
|
||||
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("+"))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
|
||||
minNumbers = initialMinNumbers + 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
|
||||
val initialSpecialChars = 1
|
||||
updateState(
|
||||
GeneratorState(
|
||||
generatedText = "Placeholder",
|
||||
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("\u2212"))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
|
||||
minSpecial = initialSpecialChars - 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, incrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
|
||||
val initialSpecialChars = 1
|
||||
updateState(
|
||||
GeneratorState(
|
||||
generatedText = "Placeholder",
|
||||
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("+"))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
|
||||
minSpecial = initialSpecialChars + 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Avoid ambiguous characters")
|
||||
.onChildren()
|
||||
.filterToOne(expectValue(Role, Switch))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleAvoidAmbigousCharactersChange(
|
||||
avoidAmbiguousChars = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Passcode Password Tests
|
||||
|
||||
//region Passcode Passphrase Tests
|
||||
|
||||
@Test
|
||||
fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() {
|
||||
val initialNumWords = 3
|
||||
|
@ -224,6 +561,8 @@ class GeneratorScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
//endregion Passcode Passphrase Tests
|
||||
|
||||
private fun updateState(state: GeneratorState) {
|
||||
mutableStateFlow.value = state
|
||||
}
|
||||
|
|
|
@ -106,6 +106,210 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class PasswordActions {
|
||||
private val defaultPasswordState = createPasswordState()
|
||||
private lateinit var viewModel: GeneratorViewModel
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
viewModel = GeneratorViewModel(initialSavedStateHandle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SliderLengthChange should update password length correctly to new value`() = runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val newLength = 16
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
|
||||
length = newLength,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
length = newLength,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToggleCapitalLettersChange should update useCapitals correctly`() = runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val useCapitals = true
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
|
||||
useCapitals = useCapitals,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
useCapitals = useCapitals,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToggleLowercaseLettersChange should update useLowercase correctly`() = runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val useLowercase = true
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleLowercaseLettersChange(
|
||||
useLowercase = useLowercase,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
useLowercase = useLowercase,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToggleNumbersChange should update useNumbers correctly`() = runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val useNumbers = true
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
|
||||
useNumbers = useNumbers,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
useNumbers = useNumbers,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToggleSpecialCharactersChange should update useSpecialChars correctly`() = runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val useSpecialChars = true
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleSpecialCharactersChange(
|
||||
useSpecialChars = useSpecialChars,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
useSpecialChars = useSpecialChars,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MinNumbersCounterChange should update minNumbers correctly`() = runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val minNumbers = 4
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
|
||||
minNumbers = minNumbers,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
minNumbers = minNumbers,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MinSpecialCharactersChange should update minSpecial correctly`() = runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val minSpecial = 2
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
|
||||
minSpecial = minSpecial,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
minSpecial = minSpecial,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToggleAvoidAmbigousCharactersChange should update avoidAmbiguousChars correctly`() =
|
||||
runTest {
|
||||
viewModel.eventFlow.test {
|
||||
val avoidAmbiguousChars = true
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleAvoidAmbigousCharactersChange(
|
||||
avoidAmbiguousChars = avoidAmbiguousChars,
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = defaultPasswordState.copy(
|
||||
generatedText = "redlohecalP",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
avoidAmbiguousChars = avoidAmbiguousChars,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class PassphraseActions {
|
||||
|
||||
|
@ -221,14 +425,32 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
//region Helper Functions
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
||||
private fun createPasswordState(
|
||||
generatedText: String = "Placeholder",
|
||||
length: Int = 10,
|
||||
length: Int = 14,
|
||||
useCapitals: Boolean = true,
|
||||
useLowercase: Boolean = true,
|
||||
useNumbers: Boolean = true,
|
||||
useSpecialChars: Boolean = false,
|
||||
minNumbers: Int = 1,
|
||||
minSpecial: Int = 1,
|
||||
avoidAmbiguousChars: Boolean = false,
|
||||
): GeneratorState =
|
||||
GeneratorState(
|
||||
generatedText = generatedText,
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(length = length),
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Password(
|
||||
length = length,
|
||||
useCapitals = useCapitals,
|
||||
useLowercase = useLowercase,
|
||||
useNumbers = useNumbers,
|
||||
useSpecialChars = useSpecialChars,
|
||||
minNumbers = minNumbers,
|
||||
minSpecial = minSpecial,
|
||||
avoidAmbiguousChars = avoidAmbiguousChars,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue