mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1054, BIT-1055: Adding modal generator UI and navigation from Add/Edit item (#643)
This commit is contained in:
parent
61e914f8ac
commit
8d5bcc4433
10 changed files with 379 additions and 54 deletions
|
@ -13,8 +13,10 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassph
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,6 +29,16 @@ interface GeneratorRepository {
|
||||||
*/
|
*/
|
||||||
val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow that represents the modal generated text.
|
||||||
|
*/
|
||||||
|
val generatorResultFlow: Flow<GeneratorResult>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the modal generator result flow to listeners.
|
||||||
|
*/
|
||||||
|
fun emitGeneratorResult(generatorResult: GeneratorResult)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to generate a password based on specifications in [passwordGeneratorRequest].
|
* Attempt to generate a password based on specifications in [passwordGeneratorRequest].
|
||||||
* The [shouldSave] flag determines if the password is saved for future reference
|
* The [shouldSave] flag determines if the password is saved for future reference
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.bitwarden.core.UsernameGeneratorRequest
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||||
|
@ -21,6 +22,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassph
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
|
@ -28,6 +30,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -51,12 +54,18 @@ class GeneratorRepositoryImpl(
|
||||||
) : GeneratorRepository {
|
) : GeneratorRepository {
|
||||||
|
|
||||||
private val scope = CoroutineScope(dispatcherManager.io)
|
private val scope = CoroutineScope(dispatcherManager.io)
|
||||||
|
|
||||||
private val mutablePasswordHistoryStateFlow =
|
private val mutablePasswordHistoryStateFlow =
|
||||||
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
|
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
|
||||||
|
|
||||||
|
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
|
||||||
|
|
||||||
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
||||||
get() = mutablePasswordHistoryStateFlow.asStateFlow()
|
get() = mutablePasswordHistoryStateFlow.asStateFlow()
|
||||||
|
|
||||||
|
override val generatorResultFlow: Flow<GeneratorResult>
|
||||||
|
get() = mutableGeneratorResultFlow.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mutablePasswordHistoryStateFlow
|
mutablePasswordHistoryStateFlow
|
||||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
||||||
|
@ -90,6 +99,10 @@ class GeneratorRepositoryImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun emitGeneratorResult(generatorResult: GeneratorResult) {
|
||||||
|
mutableGeneratorResultFlow.tryEmit(generatorResult)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun generatePassword(
|
override suspend fun generatePassword(
|
||||||
passwordGeneratorRequest: PasswordGeneratorRequest,
|
passwordGeneratorRequest: PasswordGeneratorRequest,
|
||||||
shouldSave: Boolean,
|
shouldSave: Boolean,
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.x8bit.bitwarden.data.tools.generator.repository.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A result from the Generator.
|
||||||
|
*/
|
||||||
|
sealed class GeneratorResult {
|
||||||
|
/**
|
||||||
|
* A generated username.
|
||||||
|
*/
|
||||||
|
data class Username(val username: String) : GeneratorResult()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generated password.
|
||||||
|
*/
|
||||||
|
data class Password(val password: String) : GeneratorResult()
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -55,8 +56,10 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
||||||
|
@ -72,6 +75,7 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Pa
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceTypeOption
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceTypeOption
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
@ -101,6 +105,8 @@ fun GeneratorScreen(
|
||||||
duration = SnackbarDuration.Short,
|
duration = SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GeneratorEvent.NavigateBack -> onNavigateBack.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,31 +168,36 @@ fun GeneratorScreen(
|
||||||
RandomWordHandlers.create(viewModel = viewModel)
|
RandomWordHandlers.create(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
val scrollBehavior =
|
val scrollBehavior = when (state.generatorMode) {
|
||||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
GeneratorMode.Default -> {
|
||||||
|
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
}
|
||||||
|
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BitwardenMediumTopAppBar(
|
when (state.generatorMode) {
|
||||||
title = stringResource(id = R.string.generator),
|
GeneratorMode.Modal.Username, GeneratorMode.Modal.Password ->
|
||||||
scrollBehavior = scrollBehavior,
|
ModalAppBar(
|
||||||
actions = {
|
scrollBehavior = scrollBehavior,
|
||||||
BitwardenOverflowActionItem(
|
onCloseClick = remember(viewModel) {
|
||||||
menuItemDataList = persistentListOf(
|
{ viewModel.trySendAction(GeneratorAction.CloseClick) }
|
||||||
OverflowMenuItemData(
|
},
|
||||||
text = stringResource(id = R.string.password_history),
|
onSelectClick = remember(viewModel) {
|
||||||
onClick = remember(viewModel) {
|
{ viewModel.trySendAction(GeneratorAction.SelectClick) }
|
||||||
{
|
},
|
||||||
viewModel.trySendAction(
|
|
||||||
GeneratorAction.PasswordHistoryClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
GeneratorMode.Default ->
|
||||||
|
DefaultAppBar(
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
onPasswordHistoryClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(GeneratorAction.PasswordHistoryClick) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
SnackbarHost(hostState = snackbarHostState)
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
|
@ -211,6 +222,54 @@ fun GeneratorScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//region Top App Bar Composables
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun DefaultAppBar(
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
onPasswordHistoryClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
BitwardenMediumTopAppBar(
|
||||||
|
title = stringResource(id = R.string.generator),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
actions = {
|
||||||
|
BitwardenOverflowActionItem(
|
||||||
|
menuItemDataList = persistentListOf(
|
||||||
|
OverflowMenuItemData(
|
||||||
|
text = stringResource(id = R.string.password_history),
|
||||||
|
onClick = onPasswordHistoryClick,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ModalAppBar(
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
onCloseClick: () -> Unit,
|
||||||
|
onSelectClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = stringResource(id = R.string.generator),
|
||||||
|
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||||
|
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||||
|
onNavigationIconClick = onCloseClick,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
actions = {
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.select),
|
||||||
|
onClick = onSelectClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion Top App Bar Composables
|
||||||
|
|
||||||
//region ScrollContent and Static Items
|
//region ScrollContent and Static Items
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
|
@ -242,13 +301,14 @@ private fun ScrollContent(
|
||||||
onRegenerateClick = onRegenerateClick,
|
onRegenerateClick = onRegenerateClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
if (state.generatorMode == GeneratorMode.Default) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
MainStateOptionsItem(
|
MainStateOptionsItem(
|
||||||
selectedType = state.selectedType,
|
selectedType = state.selectedType,
|
||||||
possibleMainStates = state.typeOptions,
|
possibleMainStates = state.typeOptions,
|
||||||
onMainStateOptionClicked = onMainStateOptionClicked,
|
onMainStateOptionClicked = onMainStateOptionClicked,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassph
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
@ -71,10 +72,12 @@ class GeneratorViewModel @Inject constructor(
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
|
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
|
||||||
initialState = savedStateHandle[KEY_STATE] ?: GeneratorState(
|
initialState = savedStateHandle[KEY_STATE] ?: GeneratorState(
|
||||||
generatedText = PLACEHOLDER_GENERATED_TEXT,
|
generatedText = "",
|
||||||
selectedType = Passcode(
|
selectedType = when (GeneratorArgs(savedStateHandle).type) {
|
||||||
selectedType = Password(),
|
GeneratorMode.Modal.Username -> Username()
|
||||||
),
|
GeneratorMode.Modal.Password -> Passcode()
|
||||||
|
GeneratorMode.Default -> Passcode(selectedType = Password())
|
||||||
|
},
|
||||||
generatorMode = GeneratorArgs(savedStateHandle).type,
|
generatorMode = GeneratorArgs(savedStateHandle).type,
|
||||||
currentEmailAddress =
|
currentEmailAddress =
|
||||||
requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email),
|
requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email),
|
||||||
|
@ -100,6 +103,14 @@ class GeneratorViewModel @Inject constructor(
|
||||||
handlePasswordHistoryClick()
|
handlePasswordHistoryClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is GeneratorAction.CloseClick -> {
|
||||||
|
handleCloseClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
is GeneratorAction.SelectClick -> {
|
||||||
|
handleSelectClick()
|
||||||
|
}
|
||||||
|
|
||||||
is GeneratorAction.RegenerateClick -> {
|
is GeneratorAction.RegenerateClick -> {
|
||||||
handleRegenerationClick()
|
handleRegenerationClick()
|
||||||
}
|
}
|
||||||
|
@ -198,6 +209,27 @@ class GeneratorViewModel @Inject constructor(
|
||||||
sendEvent(GeneratorEvent.NavigateToPasswordHistory)
|
sendEvent(GeneratorEvent.NavigateToPasswordHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCloseClick() {
|
||||||
|
sendEvent(GeneratorEvent.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectClick() {
|
||||||
|
when (state.selectedType) {
|
||||||
|
is Passcode -> {
|
||||||
|
generatorRepository.emitGeneratorResult(
|
||||||
|
GeneratorResult.Password(state.generatedText),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Username -> {
|
||||||
|
generatorRepository.emitGeneratorResult(
|
||||||
|
GeneratorResult.Username(state.generatedText),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendEvent(GeneratorEvent.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
//endregion Top Level Handlers
|
//endregion Top Level Handlers
|
||||||
|
|
||||||
//region Generation Handlers
|
//region Generation Handlers
|
||||||
|
@ -1394,10 +1426,6 @@ class GeneratorViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion Utility Functions
|
//endregion Utility Functions
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PLACEHOLDER_GENERATED_TEXT = "Placeholder"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1791,6 +1819,16 @@ sealed class GeneratorAction {
|
||||||
*/
|
*/
|
||||||
data object PasswordHistoryClick : GeneratorAction()
|
data object PasswordHistoryClick : GeneratorAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the user has selected a generated string from the modal generator
|
||||||
|
*/
|
||||||
|
data object SelectClick : GeneratorAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the user has clicked the close button.
|
||||||
|
*/
|
||||||
|
data object CloseClick : GeneratorAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the action to regenerate a new passcode or username.
|
* Represents the action to regenerate a new passcode or username.
|
||||||
*/
|
*/
|
||||||
|
@ -2187,6 +2225,11 @@ sealed class GeneratorEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateToPasswordHistory : GeneratorEvent()
|
data object NavigateToPasswordHistory : GeneratorEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate back to previous screen.
|
||||||
|
*/
|
||||||
|
data object NavigateBack : GeneratorEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the message in a snackbar.
|
* Displays the message in a snackbar.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -8,6 +8,8 @@ import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||||
|
@ -53,6 +55,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val clipboardManager: BitwardenClipboardManager,
|
private val clipboardManager: BitwardenClipboardManager,
|
||||||
private val vaultRepository: VaultRepository,
|
private val vaultRepository: VaultRepository,
|
||||||
|
private val generatorRepository: GeneratorRepository,
|
||||||
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
|
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
|
||||||
// We load the state from the savedStateHandle for testing purposes.
|
// We load the state from the savedStateHandle for testing purposes.
|
||||||
initialState = savedStateHandle[KEY_STATE]
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
@ -94,6 +97,14 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
.map { VaultAddEditAction.Internal.TotpCodeReceive(totpResult = it) }
|
.map { VaultAddEditAction.Internal.TotpCodeReceive(totpResult = it) }
|
||||||
.onEach(::sendAction)
|
.onEach(::sendAction)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
generatorRepository
|
||||||
|
.generatorResultFlow
|
||||||
|
.map {
|
||||||
|
VaultAddEditAction.Internal.GeneratorResultReceive(generatorResult = it)
|
||||||
|
}
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleAction(action: VaultAddEditAction) {
|
override fun handleAction(action: VaultAddEditAction) {
|
||||||
|
@ -415,13 +426,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginOpenUsernameGeneratorClick() {
|
private fun handleLoginOpenUsernameGeneratorClick() {
|
||||||
viewModelScope.launch {
|
sendEvent(event = VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username))
|
||||||
sendEvent(
|
|
||||||
event = VaultAddEditEvent.ShowToast(
|
|
||||||
message = "Open Username Generator".asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginPasswordCheckerClick() {
|
private fun handleLoginPasswordCheckerClick() {
|
||||||
|
@ -435,13 +440,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginOpenPasswordGeneratorClick() {
|
private fun handleLoginOpenPasswordGeneratorClick() {
|
||||||
viewModelScope.launch {
|
sendEvent(event = VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Password))
|
||||||
sendEvent(
|
|
||||||
event = VaultAddEditEvent.ShowToast(
|
|
||||||
message = "Open Password Generator".asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginSetupTotpClick(
|
private fun handleLoginSetupTotpClick(
|
||||||
|
@ -756,6 +755,9 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
|
|
||||||
is VaultAddEditAction.Internal.TotpCodeReceive -> handleVaultTotpCodeReceive(action)
|
is VaultAddEditAction.Internal.TotpCodeReceive -> handleVaultTotpCodeReceive(action)
|
||||||
is VaultAddEditAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
is VaultAddEditAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||||
|
is VaultAddEditAction.Internal.GeneratorResultReceive -> {
|
||||||
|
handleGeneratorResultReceive(action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -892,6 +894,27 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleGeneratorResultReceive(
|
||||||
|
action: VaultAddEditAction.Internal.GeneratorResultReceive,
|
||||||
|
) {
|
||||||
|
when (action.generatorResult) {
|
||||||
|
is GeneratorResult.Password -> {
|
||||||
|
updateLoginContent { loginType ->
|
||||||
|
loginType.copy(
|
||||||
|
password = action.generatorResult.password,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is GeneratorResult.Username -> {
|
||||||
|
updateLoginContent { loginType ->
|
||||||
|
loginType.copy(
|
||||||
|
username = action.generatorResult.username,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//endregion Internal Type Handlers
|
//endregion Internal Type Handlers
|
||||||
|
|
||||||
//region Utility Functions
|
//region Utility Functions
|
||||||
|
@ -1697,6 +1720,13 @@ sealed class VaultAddEditAction {
|
||||||
*/
|
*/
|
||||||
data class TotpCodeReceive(val totpResult: TotpCodeResult) : Internal()
|
data class TotpCodeReceive(val totpResult: TotpCodeResult) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the vault totp code result has been received.
|
||||||
|
*/
|
||||||
|
data class GeneratorResultReceive(
|
||||||
|
val generatorResult: GeneratorResult,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the vault item data has been received.
|
* Indicates that the vault item data has been received.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.bitwarden.core.PasswordGeneratorRequest
|
||||||
import com.bitwarden.core.PasswordHistoryView
|
import com.bitwarden.core.PasswordHistoryView
|
||||||
import com.bitwarden.core.UsernameGeneratorRequest
|
import com.bitwarden.core.UsernameGeneratorRequest
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
|
||||||
|
@ -12,10 +13,13 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassph
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fake implementation of [GeneratorRepository] for testing purposes.
|
* A fake implementation of [GeneratorRepository] for testing purposes.
|
||||||
|
@ -36,6 +40,8 @@ class FakeGeneratorRepository : GeneratorRepository {
|
||||||
private val mutablePasswordHistoryStateFlow =
|
private val mutablePasswordHistoryStateFlow =
|
||||||
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
|
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
|
||||||
|
|
||||||
|
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
|
||||||
|
|
||||||
private var generatePlusAddressedEmailResult: GeneratedPlusAddressedUsernameResult =
|
private var generatePlusAddressedEmailResult: GeneratedPlusAddressedUsernameResult =
|
||||||
GeneratedPlusAddressedUsernameResult.Success(
|
GeneratedPlusAddressedUsernameResult.Success(
|
||||||
generatedEmailAddress = "email+abcd1234@address.com",
|
generatedEmailAddress = "email+abcd1234@address.com",
|
||||||
|
@ -59,6 +65,13 @@ class FakeGeneratorRepository : GeneratorRepository {
|
||||||
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
||||||
get() = mutablePasswordHistoryStateFlow
|
get() = mutablePasswordHistoryStateFlow
|
||||||
|
|
||||||
|
override val generatorResultFlow: Flow<GeneratorResult>
|
||||||
|
get() = mutableGeneratorResultFlow.asSharedFlow()
|
||||||
|
|
||||||
|
override fun emitGeneratorResult(generatorResult: GeneratorResult) {
|
||||||
|
mutableGeneratorResultFlow.tryEmit(generatorResult)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun generatePassword(
|
override suspend fun generatePassword(
|
||||||
passwordGeneratorRequest: PasswordGeneratorRequest,
|
passwordGeneratorRequest: PasswordGeneratorRequest,
|
||||||
shouldSave: Boolean,
|
shouldSave: Boolean,
|
||||||
|
|
|
@ -27,6 +27,7 @@ import androidx.compose.ui.text.AnnotatedString
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
@ -58,6 +59,85 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ModalAppBar should be displayed for Password Mode`() {
|
||||||
|
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Password))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "Close")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Select")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ModalAppBar should be displayed for Username Mode`() {
|
||||||
|
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "Close")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Select")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on close click should send CloseClick`() {
|
||||||
|
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "Close")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(GeneratorAction.CloseClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on select click should send SelectClick`() {
|
||||||
|
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Select")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(GeneratorAction.SelectClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DefaultAppBar should be displayed for Default Mode`() {
|
||||||
|
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Default))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "More")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MainTypeOption select control should be hidden for password mode`() {
|
||||||
|
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Password))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MainTypeOption select control should be hidden for username mode`() {
|
||||||
|
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToPasswordHistory event should call onNavigateToPasswordHistoryScreen`() {
|
fun `NavigateToPasswordHistory event should call onNavigateToPasswordHistoryScreen`() {
|
||||||
mutableEventFlow.tryEmit(GeneratorEvent.NavigateToPasswordHistory)
|
mutableEventFlow.tryEmit(GeneratorEvent.NavigateToPasswordHistory)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.tools.feature.generator
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import app.cash.turbine.turbineScope
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
@ -12,10 +13,12 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwar
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -28,12 +31,15 @@ import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
class GeneratorViewModelTest : BaseViewModelTest() {
|
class GeneratorViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
private val initialPasscodeState = createPasswordState()
|
private val initialPasscodeState = createPasswordState()
|
||||||
private val initialPasscodeSavedStateHandle =
|
private val initialPasscodeSavedStateHandle =
|
||||||
createSavedStateHandleWithState(initialPasscodeState)
|
createSavedStateHandleWithState(initialPasscodeState)
|
||||||
|
|
||||||
|
private val initialUsernameModeState = createUsernameModeState()
|
||||||
|
|
||||||
private val initialPassphraseState = createPassphraseState()
|
private val initialPassphraseState = createPassphraseState()
|
||||||
private val passphraseSavedStateHandle = createSavedStateHandleWithState(initialPassphraseState)
|
private val passphraseSavedStateHandle = createSavedStateHandleWithState(initialPassphraseState)
|
||||||
|
|
||||||
|
@ -90,6 +96,35 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(initialPasscodeState, viewModel.stateFlow.value)
|
assertEquals(initialPasscodeState, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CloseClick should emit NavigateBack event`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.actionChannel.trySend(GeneratorAction.CloseClick)
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
val event = awaitItem()
|
||||||
|
assertEquals(GeneratorEvent.NavigateBack, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SelectClick should emit the NavigateBack event with GeneratorResult`() = runTest {
|
||||||
|
turbineScope {
|
||||||
|
val viewModel = createViewModel(state = initialUsernameModeState)
|
||||||
|
val eventTurbine = viewModel
|
||||||
|
.eventFlow
|
||||||
|
.testIn(backgroundScope)
|
||||||
|
val generatorResultTurbine = fakeGeneratorRepository
|
||||||
|
.generatorResultFlow
|
||||||
|
.testIn(backgroundScope)
|
||||||
|
|
||||||
|
viewModel.actionChannel.trySend(GeneratorAction.SelectClick)
|
||||||
|
|
||||||
|
assertEquals(GeneratorEvent.NavigateBack, eventTurbine.awaitItem())
|
||||||
|
assertEquals(GeneratorResult.Username("username"), generatorResultTurbine.awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `RegenerateClick action for password state updates generatedText and saves password generation options on successful password generation`() =
|
fun `RegenerateClick action for password state updates generatedText and saves password generation options on successful password generation`() =
|
||||||
|
@ -1550,6 +1585,21 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
||||||
currentEmailAddress = "currentEmail",
|
currentEmailAddress = "currentEmail",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun createUsernameModeState(
|
||||||
|
generatedText: String = "username",
|
||||||
|
email: String = "currentEmail",
|
||||||
|
): GeneratorState =
|
||||||
|
GeneratorState(
|
||||||
|
generatedText = generatedText,
|
||||||
|
generatorMode = GeneratorMode.Modal.Username,
|
||||||
|
selectedType = GeneratorState.MainType.Username(
|
||||||
|
GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(
|
||||||
|
email = email,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
currentEmailAddress = "currentEmail",
|
||||||
|
)
|
||||||
|
|
||||||
private fun createForwardedEmailAliasState(
|
private fun createForwardedEmailAliasState(
|
||||||
generatedText: String = "defaultForwardedEmailAlias",
|
generatedText: String = "defaultForwardedEmailAlias",
|
||||||
obfuscatedText: String = "defaultObfuscatedText",
|
obfuscatedText: String = "defaultObfuscatedText",
|
||||||
|
|
|
@ -7,6 +7,8 @@ import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||||
|
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||||
|
@ -14,6 +16,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
|
||||||
|
@ -62,6 +65,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
every { totpCodeFlow } returns totpTestCodeFlow
|
every { totpCodeFlow } returns totpTestCodeFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val generatorRepository: GeneratorRepository = FakeGeneratorRepository()
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockkStatic(CIPHER_VIEW_EXTENSIONS_PATH)
|
mockkStatic(CIPHER_VIEW_EXTENSIONS_PATH)
|
||||||
|
@ -570,7 +575,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `OpenUsernameGeneratorClick should emit ShowToast with 'Open Username Generator' message`() =
|
fun `OpenUsernameGeneratorClick should emit NavigateToGeneratorModal with username GeneratorMode`() =
|
||||||
runTest {
|
runTest {
|
||||||
val viewModel = createAddVaultItemViewModel()
|
val viewModel = createAddVaultItemViewModel()
|
||||||
|
|
||||||
|
@ -579,7 +584,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
VaultAddEditAction.ItemType.LoginType.OpenUsernameGeneratorClick,
|
VaultAddEditAction.ItemType.LoginType.OpenUsernameGeneratorClick,
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultAddEditEvent.ShowToast("Open Username Generator".asText()),
|
VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -606,7 +611,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `OpenPasswordGeneratorClick should emit ShowToast with 'Open Password Generator' message`() =
|
fun `OpenPasswordGeneratorClick should emit NavigateToGeneratorModal with with password GeneratorMode`() =
|
||||||
runTest {
|
runTest {
|
||||||
val viewModel = createAddVaultItemViewModel()
|
val viewModel = createAddVaultItemViewModel()
|
||||||
|
|
||||||
|
@ -616,7 +621,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
.trySend(VaultAddEditAction.ItemType.LoginType.OpenPasswordGeneratorClick)
|
.trySend(VaultAddEditAction.ItemType.LoginType.OpenPasswordGeneratorClick)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultAddEditEvent.ShowToast("Open Password Generator".asText()),
|
VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Password),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1186,6 +1191,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
savedStateHandle = secureNotesInitialSavedStateHandle,
|
savedStateHandle = secureNotesInitialSavedStateHandle,
|
||||||
clipboardManager = clipboardManager,
|
clipboardManager = clipboardManager,
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
|
generatorRepository = generatorRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1487,11 +1493,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
savedStateHandle: SavedStateHandle = loginInitialSavedStateHandle,
|
savedStateHandle: SavedStateHandle = loginInitialSavedStateHandle,
|
||||||
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
|
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
|
||||||
vaultRepo: VaultRepository = vaultRepository,
|
vaultRepo: VaultRepository = vaultRepository,
|
||||||
|
generatorRepo: GeneratorRepository = generatorRepository,
|
||||||
): VaultAddEditViewModel =
|
): VaultAddEditViewModel =
|
||||||
VaultAddEditViewModel(
|
VaultAddEditViewModel(
|
||||||
savedStateHandle = savedStateHandle,
|
savedStateHandle = savedStateHandle,
|
||||||
clipboardManager = bitwardenClipboardManager,
|
clipboardManager = bitwardenClipboardManager,
|
||||||
vaultRepository = vaultRepo,
|
vaultRepository = vaultRepo,
|
||||||
|
generatorRepository = generatorRepo,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue