mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 01:16:02 +03:00
PM-10692 pass a generated password back to the complete registration … (#3806)
This commit is contained in:
parent
666c165b6f
commit
76a3265bbb
10 changed files with 229 additions and 92 deletions
|
@ -10,6 +10,7 @@ import com.x8bit.bitwarden.ui.auth.feature.checkemail.checkEmailDestination
|
|||
import com.x8bit.bitwarden.ui.auth.feature.checkemail.navigateToCheckEmail
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.completeRegistrationDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.popUpToCompleteRegistration
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
|
||||
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDestination
|
||||
|
@ -184,6 +185,9 @@ fun NavGraphBuilder.authGraph(
|
|||
masterPasswordGeneratorDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToPreventLockout = { navController.navigateToPreventAccountLockout() },
|
||||
onNavigateBackWithPassword = {
|
||||
navController.popUpToCompleteRegistration()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,3 +72,10 @@ fun NavGraphBuilder.completeRegistrationDestination(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop up to the complete registration screen.
|
||||
*/
|
||||
fun NavController.popUpToCompleteRegistration() {
|
||||
popBackStack(route = COMPLETE_REGISTRATION_ROUTE, inclusive = false)
|
||||
}
|
||||
|
|
|
@ -216,7 +216,8 @@ private fun CompleteRegistrationContent(
|
|||
callToActionText = stringResource(id = R.string.learn_more),
|
||||
onCardClicked = handler.onMakeStrongPassword,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
} else {
|
||||
LegacyHeaderContent(
|
||||
|
|
|
@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
|||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
|
||||
|
@ -28,6 +30,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.isValidEmail
|
|||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -48,12 +51,12 @@ private const val MIN_PASSWORD_LENGTH = 12
|
|||
class CompleteRegistrationViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
generatorRepository: GeneratorRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: run {
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val args = CompleteRegistrationArgs(savedStateHandle)
|
||||
CompleteRegistrationState(
|
||||
userEmail = args.emailAddress,
|
||||
|
@ -91,6 +94,15 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
generatorRepository
|
||||
.generatorResultFlow
|
||||
.filterIsInstance<GeneratorResult.Password>()
|
||||
.map {
|
||||
Internal.GeneratedPasswordResult(generatedPassword = it.password)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -124,9 +136,25 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
|
||||
CompleteRegistrationAction.CallToActionClick -> handleCallToActionClick()
|
||||
is Internal.UpdateOnboardingFeatureState -> handleUpdateOnboardingFeatureState(action)
|
||||
is Internal.GeneratedPasswordResult -> handleGeneratedPasswordResult(
|
||||
action,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGeneratedPasswordResult(
|
||||
action: Internal.GeneratedPasswordResult,
|
||||
) {
|
||||
val password = action.generatedPassword
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
passwordInput = password,
|
||||
confirmPasswordInput = password,
|
||||
)
|
||||
}
|
||||
checkPasswordStrength(input = password)
|
||||
}
|
||||
|
||||
private fun verifyEmailAddress() {
|
||||
if (!state.fromEmail) {
|
||||
return
|
||||
|
@ -262,21 +290,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
private fun handlePasswordInputChanged(action: PasswordInputChange) {
|
||||
// Update input:
|
||||
mutableStateFlow.update { it.copy(passwordInput = action.input) }
|
||||
// Update password strength:
|
||||
passwordStrengthJob.cancel()
|
||||
if (action.input.isEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
|
||||
}
|
||||
} else {
|
||||
passwordStrengthJob = viewModelScope.launch {
|
||||
val result = authRepository.getPasswordStrength(
|
||||
email = state.userEmail,
|
||||
password = action.input,
|
||||
)
|
||||
trySendAction(ReceivePasswordStrengthResult(result))
|
||||
}
|
||||
}
|
||||
checkPasswordStrength(action.input)
|
||||
}
|
||||
|
||||
private fun handleConfirmPasswordInputChanged(action: ConfirmPasswordInputChange) {
|
||||
|
@ -367,6 +381,24 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPasswordStrength(input: String) {
|
||||
// Update password strength:
|
||||
passwordStrengthJob.cancel()
|
||||
if (input.isEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
|
||||
}
|
||||
} else {
|
||||
passwordStrengthJob = viewModelScope.launch {
|
||||
val result = authRepository.getPasswordStrength(
|
||||
email = state.userEmail,
|
||||
password = input,
|
||||
)
|
||||
trySendAction(ReceivePasswordStrengthResult(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -564,5 +596,10 @@ sealed class CompleteRegistrationAction {
|
|||
* Indicate on boarding feature state has been updated.
|
||||
*/
|
||||
data class UpdateOnboardingFeatureState(val newValue: Boolean) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a generated password has been received.
|
||||
*/
|
||||
data class GeneratedPasswordResult(val generatedPassword: String) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ fun NavController.navigateToMasterPasswordGenerator(navOptions: NavOptions? = nu
|
|||
fun NavGraphBuilder.masterPasswordGeneratorDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToPreventLockout: () -> Unit,
|
||||
onNavigateBackWithPassword: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = MASTER_PASSWORD_GENERATOR,
|
||||
|
@ -27,6 +28,7 @@ fun NavGraphBuilder.masterPasswordGeneratorDestination(
|
|||
MasterPasswordGeneratorScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToPreventLockout = onNavigateToPreventLockout,
|
||||
onNavigateBackWithPassword = onNavigateBackWithPassword,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
|||
fun MasterPasswordGeneratorScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToPreventLockout: () -> Unit,
|
||||
onNavigateBackWithPassword: () -> Unit,
|
||||
viewModel: MasterPasswordGeneratorViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
@ -71,6 +72,10 @@ fun MasterPasswordGeneratorScreen(
|
|||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
}
|
||||
|
||||
is MasterPasswordGeneratorEvent.NavigateBackToRegistration -> {
|
||||
onNavigateBackWithPassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
|||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -73,7 +74,8 @@ class MasterPasswordGeneratorViewModel @Inject constructor(
|
|||
private fun handleBackAction() = sendEvent(MasterPasswordGeneratorEvent.NavigateBack)
|
||||
|
||||
private fun handleSavePasswordAction() {
|
||||
// TODO [PM-10692](https://bitwarden.atlassian.net/browse/PM-10692)
|
||||
generatorRepository.emitGeneratorResult(GeneratorResult.Password(state.generatedPassword))
|
||||
sendEvent(MasterPasswordGeneratorEvent.NavigateBackToRegistration)
|
||||
}
|
||||
|
||||
private fun handlePreventLockoutAction() =
|
||||
|
@ -163,6 +165,11 @@ sealed class MasterPasswordGeneratorEvent {
|
|||
* Show a Snackbar message.
|
||||
*/
|
||||
data class ShowSnackbar(val text: Text) : MasterPasswordGeneratorEvent()
|
||||
|
||||
/**
|
||||
* Navigate back to the complete registration screen.
|
||||
*/
|
||||
data object NavigateBackToRegistration : MasterPasswordGeneratorEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,9 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
|||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
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.model.GeneratorResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.Internal.ReceivePasswordStrengthResult
|
||||
|
@ -61,6 +64,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
|
||||
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow
|
||||
}
|
||||
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
|
||||
private val generatorRepository = mockk<GeneratorRepository>(relaxed = true) {
|
||||
every { generatorResultFlow } returns mutableGeneratorResultFlow
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
|
@ -95,14 +102,14 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
environmentRepository = fakeEnvironmentRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
generatorRepository = generatorRepository,
|
||||
)
|
||||
viewModel.onCleared()
|
||||
assertTrue(specialCircumstanceManager.specialCircumstance == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Password below 12 chars should have non-valid state`() =
|
||||
runTest {
|
||||
fun `Password below 12 chars should have non-valid state`() = runTest {
|
||||
val input = "abcdefghikl"
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength(EMAIL, input)
|
||||
|
@ -114,8 +121,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `Passwords not matching should have non-valid state`() =
|
||||
runTest {
|
||||
fun `Passwords not matching should have non-valid state`() = runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD)
|
||||
} returns PasswordStrengthResult.Error
|
||||
|
@ -344,8 +350,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
} returns RegisterResult.WeakPassword
|
||||
}
|
||||
val initialState = VALID_INPUT_STATE
|
||||
.copy(
|
||||
val initialState = VALID_INPUT_STATE.copy(
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
isCheckDataBreachesToggled = true,
|
||||
)
|
||||
|
@ -418,6 +423,47 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
coVerify { mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Empty PasswordInputChange update should result in password strength being NONE`() =
|
||||
runTest {
|
||||
val viewModel = createCompleteRegistrationViewModel(
|
||||
completeRegistrationState = DEFAULT_STATE.copy(
|
||||
passwordInput = PASSWORD,
|
||||
passwordStrengthState = PasswordStrengthState.STRONG,
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(PasswordInputChange(""))
|
||||
val expectedStrengthUpdateState = DEFAULT_STATE.copy(
|
||||
passwordInput = "",
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedStrengthUpdateState, awaitItem())
|
||||
}
|
||||
coVerify(exactly = 0) { mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Internal GeneratedPasswordResult update passwordInput and confirmPasswordInput and call getPasswordStrength`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD)
|
||||
} returns PasswordStrengthResult.Error
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
mutableGeneratorResultFlow.emit(GeneratorResult.Password(PASSWORD))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = PASSWORD,
|
||||
confirmPasswordInput = PASSWORD,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
coVerify { mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CheckDataBreachesToggle should change isCheckDataBreachesToggled`() = runTest {
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
|
@ -565,8 +611,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick with no email not should show dialog`() =
|
||||
runTest {
|
||||
fun `CreateAccountClick with no email not should show dialog`() = runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength("", PASSWORD)
|
||||
} returns PasswordStrengthResult.Error
|
||||
|
@ -580,8 +625,9 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
dialog = CompleteRegistrationDialog.Error(
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.email_address.asText()),
|
||||
message = R.string.validation_field_required.asText(
|
||||
R.string.email_address.asText(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -594,8 +640,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
private fun createCompleteRegistrationViewModel(
|
||||
completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE,
|
||||
authRepository: AuthRepository = mockAuthRepository,
|
||||
): CompleteRegistrationViewModel =
|
||||
CompleteRegistrationViewModel(
|
||||
): CompleteRegistrationViewModel = CompleteRegistrationViewModel(
|
||||
savedStateHandle = SavedStateHandle(
|
||||
mapOf(
|
||||
"state" to completeRegistrationState,
|
||||
|
@ -605,6 +650,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
environmentRepository = fakeEnvironmentRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
generatorRepository = generatorRepository,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.junit.Test
|
|||
class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToPreventLockoutCalled = false
|
||||
private var navigateBackWithPasswordCalled = false
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<MasterPasswordGeneratorEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(
|
||||
value = MasterPasswordGeneratorState(generatedPassword = "-"),
|
||||
|
@ -35,6 +36,7 @@ class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
|
|||
MasterPasswordGeneratorScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToPreventLockout = { onNavigateToPreventLockoutCalled = true },
|
||||
onNavigateBackWithPassword = { navigateBackWithPasswordCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
@ -42,7 +44,7 @@ class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `Generated password field state should update with ViewModel state`() {
|
||||
val updatedValue = "soup-r-stronk-pazzwerd"
|
||||
val updatedValue = PASSWORD_INPUT
|
||||
mutableStateFlow.update { it.copy(generatedPassword = updatedValue) }
|
||||
|
||||
composeTestRule
|
||||
|
@ -104,4 +106,15 @@ class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
|
|||
|
||||
verify { viewModel.trySendAction(MasterPasswordGeneratorAction.PreventLockoutClickAction) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Verify navigating back with password invokes the lambda`() {
|
||||
mutableEventFlow.tryEmit(
|
||||
MasterPasswordGeneratorEvent.NavigateBackToRegistration,
|
||||
)
|
||||
|
||||
assertTrue(navigateBackWithPasswordCalled)
|
||||
}
|
||||
}
|
||||
|
||||
private const val PASSWORD_INPUT = "password1234"
|
||||
|
|
|
@ -124,6 +124,21 @@ class MasterPasswordGeneratorViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBackWithPassword event is sent when SavePasswordClickAction is handled`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel(
|
||||
initialState = MasterPasswordGeneratorState(generatedPassword = "saved-pw"),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(MasterPasswordGeneratorAction.SavePasswordClickAction)
|
||||
assertEquals(
|
||||
MasterPasswordGeneratorEvent.NavigateBackToRegistration,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region helpers
|
||||
|
||||
private fun createViewModel(
|
||||
|
|
Loading…
Reference in a new issue