mirror of
https://github.com/bitwarden/android.git
synced 2026-06-06 22:42:58 -05:00
BIT-1524, BIT-898: Update generated text (#964)
This commit is contained in:
committed by
Álison Fernandes
parent
6294e656ce
commit
dc2e07c130
@@ -18,6 +18,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -33,6 +34,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -470,7 +472,7 @@ private fun PasscodeOptionsItem(
|
||||
currentSubState: GeneratorState.MainType.Passcode,
|
||||
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
|
||||
) {
|
||||
val possibleSubStates = GeneratorState.MainType.Passcode.PasscodeTypeOption.values().toList()
|
||||
val possibleSubStates = GeneratorState.MainType.Passcode.PasscodeTypeOption.entries
|
||||
val optionsWithStrings = possibleSubStates.associateWith { stringResource(id = it.labelRes) }
|
||||
|
||||
BitwardenMultiSelectButton(
|
||||
@@ -622,6 +624,12 @@ private fun PasswordLengthSliderItem(
|
||||
},
|
||||
valueRange = sliderRange,
|
||||
steps = maxValue - 1,
|
||||
colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
disabledActiveTickColor = Color.Transparent,
|
||||
disabledInactiveTickColor = Color.Transparent,
|
||||
),
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "PasswordLengthSlider" }
|
||||
.weight(1f),
|
||||
@@ -782,8 +790,7 @@ private fun ColumnScope.PassphraseTypeContent(
|
||||
|
||||
PassphraseWordSeparatorInputItem(
|
||||
wordSeparator = passphraseTypeState.wordSeparator,
|
||||
onPassphraseWordSeparatorChange =
|
||||
passphraseHandlers.onPassphraseWordSeparatorChange,
|
||||
onPassphraseWordSeparatorChange = passphraseHandlers.onPassphraseWordSeparatorChange,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -835,13 +842,19 @@ private fun PassphraseWordSeparatorInputItem(
|
||||
) {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.word_separator),
|
||||
value = wordSeparator?.toString() ?: "",
|
||||
value = wordSeparator?.toString().orEmpty(),
|
||||
onValueChange = {
|
||||
onPassphraseWordSeparatorChange(it.toCharArray().firstOrNull())
|
||||
// When onValueChange triggers and we don't update the value for whatever reason,
|
||||
// onValueChange triggers again with the previous value.
|
||||
// To avoid passphrase regeneration, we filter out those re-emissions.
|
||||
val char = it.firstOrNull()
|
||||
if (char != wordSeparator) {
|
||||
onPassphraseWordSeparatorChange(char)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "WordSeparatorEntry" }
|
||||
.width(267.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
private const val NO_GENERATED_TEXT: String = "-"
|
||||
|
||||
/**
|
||||
* ViewModel responsible for handling user interactions in the generator screen.
|
||||
@@ -77,7 +78,7 @@ class GeneratorViewModel @Inject constructor(
|
||||
private val policyManager: PolicyManager,
|
||||
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: GeneratorState(
|
||||
generatedText = "",
|
||||
generatedText = NO_GENERATED_TEXT,
|
||||
selectedType = when (GeneratorArgs(savedStateHandle).type) {
|
||||
GeneratorMode.Modal.Username -> Username()
|
||||
GeneratorMode.Modal.Password -> Passcode()
|
||||
@@ -309,7 +310,7 @@ class GeneratorViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUsernameOptions(selectedType: Username) {
|
||||
private fun loadUsernameOptions(selectedType: Username, forceRegeneration: Boolean = false) {
|
||||
val options = generatorRepository.getUsernameGenerationOptions()
|
||||
val updatedSelectedType = when (val type = selectedType.selectedType) {
|
||||
is PlusAddressedEmail -> {
|
||||
@@ -351,7 +352,7 @@ class GeneratorViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
mutableStateFlow.update { it.copy(selectedType = updatedSelectedType) }
|
||||
updateGeneratorMainType(forceRegeneration = forceRegeneration) { updatedSelectedType }
|
||||
}
|
||||
|
||||
private fun savePasswordOptionsToDisk(password: Password) {
|
||||
@@ -520,7 +521,7 @@ class GeneratorViewModel @Inject constructor(
|
||||
private suspend fun generatePassphrase(passphrase: Passphrase) {
|
||||
val request = PassphraseGeneratorRequest(
|
||||
numWords = passphrase.numWords.toUByte(),
|
||||
wordSeparator = passphrase.wordSeparator.toString(),
|
||||
wordSeparator = passphrase.wordSeparator?.toString() ?: " ",
|
||||
capitalize = passphrase.capitalize,
|
||||
includeNumber = passphrase.includeNumber,
|
||||
)
|
||||
@@ -536,7 +537,7 @@ class GeneratorViewModel @Inject constructor(
|
||||
private fun handleRegenerationClick() {
|
||||
// Go through the update process with the current state to trigger a
|
||||
// regeneration of the generated text for the same state.
|
||||
updateGeneratorMainType(isManualRegeneration = true) { mutableStateFlow.value.selectedType }
|
||||
updateGeneratorMainType(forceRegeneration = true) { mutableStateFlow.value.selectedType }
|
||||
}
|
||||
|
||||
private fun handleCopyClick() {
|
||||
@@ -650,10 +651,12 @@ class GeneratorViewModel @Inject constructor(
|
||||
private fun handleMainTypeOptionSelect(action: GeneratorAction.MainTypeOptionSelect) {
|
||||
when (action.mainTypeOption) {
|
||||
GeneratorState.MainTypeOption.PASSWORD -> {
|
||||
loadPasscodeOptions(Passcode(), usePolicyDefault = true)
|
||||
loadPasscodeOptions(selectedType = Passcode(), usePolicyDefault = true)
|
||||
}
|
||||
|
||||
GeneratorState.MainTypeOption.USERNAME -> loadUsernameOptions(Username())
|
||||
GeneratorState.MainTypeOption.USERNAME -> {
|
||||
loadUsernameOptions(selectedType = Username(), forceRegeneration = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,18 +886,24 @@ class GeneratorViewModel @Inject constructor(
|
||||
when (action.usernameTypeOption) {
|
||||
Username.UsernameTypeOption.PLUS_ADDRESSED_EMAIL -> loadUsernameOptions(
|
||||
selectedType = Username(selectedType = PlusAddressedEmail()),
|
||||
forceRegeneration = true,
|
||||
)
|
||||
|
||||
Username.UsernameTypeOption.CATCH_ALL_EMAIL -> loadUsernameOptions(
|
||||
selectedType = Username(selectedType = CatchAllEmail()),
|
||||
forceRegeneration = true,
|
||||
)
|
||||
|
||||
// We do not force regeneration here since the API can fail if the data is entered
|
||||
// incorrectly. This will only be generated when the user clicks the regenerate button.
|
||||
Username.UsernameTypeOption.FORWARDED_EMAIL_ALIAS -> loadUsernameOptions(
|
||||
selectedType = Username(selectedType = ForwardedEmailAlias()),
|
||||
forceRegeneration = false,
|
||||
)
|
||||
|
||||
Username.UsernameTypeOption.RANDOM_WORD -> loadUsernameOptions(
|
||||
selectedType = Username(selectedType = RandomWord()),
|
||||
forceRegeneration = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1170,7 +1179,7 @@ class GeneratorViewModel @Inject constructor(
|
||||
//region Utility Functions
|
||||
|
||||
private inline fun updateGeneratorMainType(
|
||||
isManualRegeneration: Boolean = false,
|
||||
forceRegeneration: Boolean = false,
|
||||
crossinline block: (GeneratorState.MainType) -> GeneratorState.MainType?,
|
||||
) {
|
||||
val currentSelectedType = mutableStateFlow.value.selectedType
|
||||
@@ -1195,30 +1204,34 @@ class GeneratorViewModel @Inject constructor(
|
||||
is Username -> when (val selectedType = updatedMainType.selectedType) {
|
||||
is ForwardedEmailAlias -> {
|
||||
saveForwardedEmailAliasServiceTypeToDisk(selectedType)
|
||||
if (isManualRegeneration) {
|
||||
if (forceRegeneration) {
|
||||
generateForwardedEmailAlias(selectedType)
|
||||
} else {
|
||||
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
|
||||
}
|
||||
}
|
||||
|
||||
is CatchAllEmail -> {
|
||||
saveCatchAllEmailOptionsToDisk(selectedType)
|
||||
if (isManualRegeneration) {
|
||||
if (forceRegeneration) {
|
||||
generateCatchAllEmail(selectedType)
|
||||
} else {
|
||||
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
|
||||
}
|
||||
}
|
||||
|
||||
is PlusAddressedEmail -> {
|
||||
savePlusAddressedEmailOptionsToDisk(selectedType)
|
||||
if (isManualRegeneration) {
|
||||
if (forceRegeneration) {
|
||||
generatePlusAddressedEmail(selectedType)
|
||||
} else {
|
||||
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
|
||||
}
|
||||
}
|
||||
|
||||
is RandomWord -> {
|
||||
saveRandomWordOptionsToDisk(selectedType)
|
||||
if (isManualRegeneration) {
|
||||
generateRandomWordUsername(selectedType)
|
||||
}
|
||||
generateRandomWordUsername(selectedType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1226,7 +1239,10 @@ class GeneratorViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun generateForwardedEmailAlias(alias: ForwardedEmailAlias) {
|
||||
val request = alias.selectedServiceType?.toUsernameGeneratorRequest() ?: return
|
||||
val request = alias.selectedServiceType?.toUsernameGeneratorRequest() ?: run {
|
||||
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
|
||||
return
|
||||
}
|
||||
val result = generatorRepository.generateForwardedServiceUsername(request)
|
||||
sendAction(GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult(result))
|
||||
}
|
||||
@@ -1242,10 +1258,14 @@ class GeneratorViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun generateCatchAllEmail(catchAllEmail: CatchAllEmail) {
|
||||
val domainName = catchAllEmail.domainName.orNullIfBlank() ?: run {
|
||||
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
|
||||
return
|
||||
}
|
||||
val result = generatorRepository.generateCatchAllEmail(
|
||||
UsernameGeneratorRequest.Catchall(
|
||||
type = AppendType.Random,
|
||||
domain = catchAllEmail.domainName,
|
||||
domain = domainName,
|
||||
),
|
||||
)
|
||||
sendAction(GeneratorAction.Internal.UpdateGeneratedCatchAllUsernameResult(result))
|
||||
@@ -1498,7 +1518,7 @@ data class GeneratorState(
|
||||
* Provides a list of available main types for the generator.
|
||||
*/
|
||||
val typeOptions: List<MainTypeOption>
|
||||
get() = MainTypeOption.values().toList()
|
||||
get() = MainTypeOption.entries.toList()
|
||||
|
||||
/**
|
||||
* Enum representing the main type options for the generator, such as PASSWORD and USERNAME.
|
||||
|
||||
@@ -2,18 +2,22 @@ package com.x8bit.bitwarden.ui.tools.feature.generator.util
|
||||
|
||||
import com.bitwarden.generators.ForwarderServiceType
|
||||
import com.bitwarden.generators.UsernameGeneratorRequest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType
|
||||
|
||||
/**
|
||||
* Converts a [ServiceType] to a [UsernameGeneratorRequest.Forwarded].
|
||||
*/
|
||||
fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded {
|
||||
@Suppress("ReturnCount", "LongMethod")
|
||||
fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded? {
|
||||
return when (this) {
|
||||
is ServiceType.AddyIo -> {
|
||||
val accessToken = this.apiAccessToken.orNullIfBlank() ?: return null
|
||||
val domain = this.domainName.orNullIfBlank() ?: return null
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.AddyIo(
|
||||
apiToken = this.apiAccessToken,
|
||||
domain = this.domainName,
|
||||
apiToken = accessToken,
|
||||
domain = domain,
|
||||
baseUrl = this.baseUrl,
|
||||
),
|
||||
website = null,
|
||||
@@ -21,31 +25,51 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
|
||||
}
|
||||
|
||||
is ServiceType.DuckDuckGo -> {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.DuckDuckGo(token = this.apiKey),
|
||||
website = null,
|
||||
)
|
||||
this
|
||||
.apiKey
|
||||
.orNullIfBlank()
|
||||
?.let {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.DuckDuckGo(token = it),
|
||||
website = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ServiceType.FirefoxRelay -> {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.Firefox(apiToken = this.apiAccessToken),
|
||||
website = null,
|
||||
)
|
||||
this
|
||||
.apiAccessToken
|
||||
.orNullIfBlank()
|
||||
?.let {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.Firefox(apiToken = it),
|
||||
website = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ServiceType.FastMail -> {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.Fastmail(apiToken = this.apiKey),
|
||||
website = null,
|
||||
)
|
||||
this
|
||||
.apiKey
|
||||
.orNullIfBlank()
|
||||
?.let {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.Fastmail(apiToken = it),
|
||||
website = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ServiceType.SimpleLogin -> {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.SimpleLogin(apiKey = this.apiKey),
|
||||
website = null,
|
||||
)
|
||||
this
|
||||
.apiKey
|
||||
.orNullIfBlank()
|
||||
?.let {
|
||||
UsernameGeneratorRequest.Forwarded(
|
||||
service = ForwarderServiceType.SimpleLogin(apiKey = it),
|
||||
website = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user