PM-10617 + PM-10637 update complete registration screen to match new onboarding design (#3787)

This commit is contained in:
Dave Severns 2024-08-21 15:32:28 -04:00 committed by GitHub
parent 13b256d4e9
commit 075956ce17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 883 additions and 299 deletions

View file

@ -27,6 +27,7 @@ import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDe
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordgenerator.masterPasswordGeneratorDestination
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordgenerator.navigateToMasterPasswordGenerator
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordguidance.masterPasswordGuidanceDestination
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordguidance.navigateToMasterPasswordGuidance
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.masterPasswordHintDestination
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
import com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout.navigateToPreventAccountLockout
@ -83,8 +84,20 @@ fun NavGraphBuilder.authGraph(
)
completeRegistrationDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToLanding = {
navController.popBackStack(route = LANDING_ROUTE, inclusive = false)
onNavigateToPasswordGuidance = {
navController.navigateToMasterPasswordGuidance()
},
onNavigateToPreventAccountLockout = {
navController.navigateToPreventAccountLockout()
},
onNavigateToLogin = { emailAddress, captchaToken ->
navController.navigateToLogin(
emailAddress = emailAddress,
captchaToken = captchaToken,
navOptions = navOptions {
popUpTo(LANDING_ROUTE)
},
)
},
)
enterpriseSignOnDestination(

View file

@ -52,7 +52,9 @@ fun NavController.navigateToCompleteRegistration(
*/
fun NavGraphBuilder.completeRegistrationDestination(
onNavigateBack: () -> Unit,
onNavigateToLanding: () -> Unit,
onNavigateToPasswordGuidance: () -> Unit,
onNavigateToPreventAccountLockout: () -> Unit,
onNavigateToLogin: (email: String, token: String) -> Unit,
) {
composableWithSlideTransitions(
route = COMPLETE_REGISTRATION_ROUTE,
@ -64,7 +66,9 @@ fun NavGraphBuilder.completeRegistrationDestination(
) {
CompleteRegistrationScreen(
onNavigateBack = onNavigateBack,
onNavigateToLanding = onNavigateToLanding,
onNavigateToPasswordGuidance = onNavigateToPasswordGuidance,
onNavigateToPreventAccountLockout = onNavigateToPreventAccountLockout,
onNavigateToLogin = onNavigateToLogin,
)
}
}

View file

@ -1,7 +1,10 @@
package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import android.content.res.Configuration
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -9,6 +12,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@ -19,30 +23,30 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ContinueWithBreachedPasswordClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CreateAccountClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ErrorDialogDismiss
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.handlers.CompleteRegistrationHandler
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.handlers.rememberCompleteRegistrationHandler
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
@ -50,10 +54,12 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.theme.nonMaterialTypography
import com.x8bit.bitwarden.ui.platform.util.isPortrait
/**
* Top level composable for the complete registration screen.
@ -63,11 +69,13 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@Composable
fun CompleteRegistrationScreen(
onNavigateBack: () -> Unit,
onNavigateToLanding: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
onNavigateToPasswordGuidance: () -> Unit,
onNavigateToPreventAccountLockout: () -> Unit,
onNavigateToLogin: (email: String, token: String) -> Unit,
viewModel: CompleteRegistrationViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val handler = rememberCompleteRegistrationHandler(viewModel = viewModel)
val context = LocalContext.current
EventsEffect(viewModel) { event ->
when (event) {
@ -76,8 +84,16 @@ fun CompleteRegistrationScreen(
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
}
is CompleteRegistrationEvent.NavigateToLanding -> {
onNavigateToLanding()
CompleteRegistrationEvent.NavigateToMakePasswordStrong -> onNavigateToPasswordGuidance()
CompleteRegistrationEvent.NavigateToPreventAccountLockout -> {
onNavigateToPreventAccountLockout()
}
is CompleteRegistrationEvent.NavigateToLogin -> {
onNavigateToLogin(
event.email,
event.captchaToken,
)
}
}
}
@ -87,9 +103,7 @@ fun CompleteRegistrationScreen(
is CompleteRegistrationDialog.Error -> {
BitwardenBasicDialog(
visibilityState = dialog.state,
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(ErrorDialogDismiss) }
},
onDismissRequest = handler.onDismissErrorDialog,
)
}
@ -99,15 +113,9 @@ fun CompleteRegistrationScreen(
message = dialog.message(),
confirmButtonText = stringResource(id = R.string.yes),
dismissButtonText = stringResource(id = R.string.no),
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(ContinueWithBreachedPasswordClick) }
},
onDismissClick = remember(viewModel) {
{ viewModel.trySendAction(ErrorDialogDismiss) }
},
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(ErrorDialogDismiss) }
},
onConfirmClick = handler.onContinueWithBreachedPasswordClick,
onDismissClick = handler.onDismissErrorDialog,
onDismissRequest = handler.onDismissErrorDialog,
)
}
@ -127,22 +135,11 @@ fun CompleteRegistrationScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.set_password),
title = stringResource(id = R.string.create_account),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(CloseClick) }
},
actions = {
BitwardenTextButton(
label = stringResource(id = R.string.create_account),
onClick = remember(viewModel) {
{ viewModel.trySendAction(CreateAccountClick) }
},
modifier = Modifier.testTag("CreateAccountButton"),
)
},
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = handler.onBackClick,
)
},
) { innerPadding ->
@ -153,84 +150,198 @@ fun CompleteRegistrationScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(16.dp))
@Suppress("MaxLineLength")
Text(
text = stringResource(
id = R.string.follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account,
state.userEmail,
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
var showPassword by rememberSaveable { mutableStateOf(false) }
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
showPassword = showPassword,
showPasswordChange = { showPassword = it },
value = state.passwordInput,
hint = state.passwordLengthLabel(),
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordInputChange(it)) }
},
modifier = Modifier
.testTag("MasterPasswordEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
showPasswordTestTag = "PasswordVisibilityToggle",
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.confirmPasswordInput,
showPassword = showPassword,
showPasswordChange = { showPassword = it },
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
},
modifier = Modifier
.testTag("ConfirmMasterPasswordEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = state.passwordHintInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordHintChange(it)) }
},
hint = stringResource(id = R.string.master_password_hint_description),
modifier = Modifier
.testTag("MasterPasswordHintLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenSwitch(
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
isChecked = state.isCheckDataBreachesToggled,
onCheckedChange = remember(viewModel) {
{ newState ->
viewModel.trySendAction(CheckDataBreachesToggle(newState = newState))
}
},
modifier = Modifier
.testTag("CheckExposedMasterPasswordToggle")
.padding(horizontal = 16.dp),
CompleteRegistrationContent(
passwordInput = state.passwordInput,
passwordStrengthState = state.passwordStrengthState,
confirmPasswordInput = state.confirmPasswordInput,
passwordHintInput = state.passwordHintInput,
isCheckDataBreachesToggled = state.isCheckDataBreachesToggled,
handler = handler,
modifier = Modifier.standardHorizontalMargin(),
nextButtonEnabled = state.hasValidMasterPassword,
callToActionText = state.callToActionText(),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@Suppress("LongMethod")
@Composable
private fun CompleteRegistrationContent(
passwordInput: String,
passwordStrengthState: PasswordStrengthState,
confirmPasswordInput: String,
passwordHintInput: String,
isCheckDataBreachesToggled: Boolean,
nextButtonEnabled: Boolean,
callToActionText: String,
handler: CompleteRegistrationHandler,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth(),
) {
Spacer(modifier = Modifier.height(8.dp))
CompleteRegistrationContentHeader(
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenActionCard(
actionIcon = rememberVectorPainter(id = R.drawable.ic_tooltip),
actionText = stringResource(id = R.string.what_makes_a_password_strong),
callToActionText = stringResource(id = R.string.learn_more),
onCardClicked = handler.onMakeStrongPassword,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
var showPassword by rememberSaveable { mutableStateOf(false) }
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
showPassword = showPassword,
showPasswordChange = { showPassword = it },
value = passwordInput,
onValueChange = handler.onPasswordInputChange,
modifier = Modifier
.testTag("MasterPasswordEntry")
.fillMaxWidth(),
showPasswordTestTag = "PasswordVisibilityToggle",
imeAction = ImeAction.Next,
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = passwordStrengthState,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = confirmPasswordInput,
showPassword = showPassword,
showPasswordChange = { showPassword = it },
onValueChange = handler.onConfirmPasswordInputChange,
modifier = Modifier
.testTag("ConfirmMasterPasswordEntry")
.fillMaxWidth(),
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = passwordHintInput,
onValueChange = handler.onPasswordHintChange,
hint = stringResource(
R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password,
),
modifier = Modifier
.testTag("MasterPasswordHintLabel")
.fillMaxWidth(),
)
BitwardenClickableText(
label = stringResource(id = R.string.learn_about_other_ways_to_prevent_account_lockout),
onClick = handler.onLearnToPreventLockout,
style = nonMaterialTypography.labelMediumProminent,
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenSwitch(
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
isChecked = isCheckDataBreachesToggled,
onCheckedChange = handler.onCheckDataBreachesToggle,
modifier = Modifier.testTag("CheckExposedMasterPasswordToggle"),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenFilledButton(
label = callToActionText,
isEnabled = nextButtonEnabled,
onClick = handler.onCallToAction,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun CompleteRegistrationContentHeader(
modifier: Modifier = Modifier,
configuration: Configuration = LocalConfiguration.current,
) {
if (configuration.isPortrait) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
OrderedHeaderContent()
}
} else {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
OrderedHeaderContent()
}
}
}
/**
* Header content ordered with the image "first" and the text "second" which can be placed in a
* [Column] or [Row].
*/
@Composable
private fun OrderedHeaderContent() {
Image(
painter = rememberVectorPainter(id = R.drawable.lock),
contentDescription = null,
modifier = Modifier.size(100.dp),
)
Spacer(modifier = Modifier.size(24.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.choose_your_master_password),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(
R.string.choose_a_unique_and_strong_password_to_keep_your_information_safe,
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
)
}
}
@PreviewScreenSizes
@Composable
private fun CompleteRegistrationContentPreview() {
BitwardenTheme {
CompleteRegistrationContent(
passwordInput = "tortor",
passwordStrengthState = PasswordStrengthState.WEAK_3,
confirmPasswordInput = "consequat",
passwordHintInput = "dissentiunt",
isCheckDataBreachesToggled = false,
handler = CompleteRegistrationHandler(
onDismissErrorDialog = {},
onContinueWithBreachedPasswordClick = {},
onBackClick = {},
onPasswordInputChange = {},
onConfirmPasswordInputChange = {},
onPasswordHintChange = {},
onCheckDataBreachesToggle = {},
onLearnToPreventLockout = {},
onMakeStrongPassword = {},
onCallToAction = {},
),
callToActionText = "Next",
nextButtonEnabled = true,
modifier = Modifier.standardHorizontalMargin(),
)
}
}

View file

@ -8,13 +8,14 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
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.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.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ContinueWithBreachedPasswordClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CreateAccountClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ErrorDialogDismiss
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.Internal
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.Internal.ReceivePasswordStrengthResult
@ -23,7 +24,6 @@ import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistra
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
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.base.util.isValidEmail
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
import dagger.hilt.android.lifecycle.HiltViewModel
@ -40,12 +40,13 @@ private const val KEY_STATE = "state"
private const val MIN_PASSWORD_LENGTH = 12
/**
* Models logic for the create account screen.
* Models logic for the Complete Registration screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class CompleteRegistrationViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
featureFlagManager: FeatureFlagManager,
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager,
@ -63,6 +64,7 @@ class CompleteRegistrationViewModel @Inject constructor(
isCheckDataBreachesToggled = true,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
onBoardingEnabled = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow),
)
},
) {
@ -90,11 +92,10 @@ class CompleteRegistrationViewModel @Inject constructor(
override fun handleAction(action: CompleteRegistrationAction) {
when (action) {
is CreateAccountClick -> handleCreateAccountClick()
is ConfirmPasswordInputChange -> handleConfirmPasswordInputChanged(action)
is PasswordHintChange -> handlePasswordHintChanged(action)
is PasswordInputChange -> handlePasswordInputChanged(action)
is CloseClick -> handleCloseClick()
is BackClick -> handleBackClicked()
is ErrorDialogDismiss -> handleDialogDismiss()
is CheckDataBreachesToggle -> handleCheckDataBreachesToggle(action)
is Internal.ReceiveRegisterResult -> {
@ -103,6 +104,15 @@ class CompleteRegistrationViewModel @Inject constructor(
ContinueWithBreachedPasswordClick -> handleContinueWithBreachedPasswordClick()
is ReceivePasswordStrengthResult -> handlePasswordStrengthResult(action)
CompleteRegistrationAction.LearnToPreventLockoutClick -> {
handlePreventAccountLockoutClickAction()
}
CompleteRegistrationAction.MakePasswordStrongClick -> {
handleMakePasswordStrongClickAction()
}
CompleteRegistrationAction.CallToActionClick -> handleCallToActionClick()
}
}
@ -170,7 +180,10 @@ class CompleteRegistrationViewModel @Inject constructor(
is RegisterResult.Success -> {
mutableStateFlow.update { it.copy(dialog = null) }
sendEvent(
CompleteRegistrationEvent.NavigateToLanding,
CompleteRegistrationEvent.NavigateToLogin(
email = state.userEmail,
captchaToken = registerAccountResult.captchaToken,
),
)
}
@ -221,7 +234,7 @@ class CompleteRegistrationViewModel @Inject constructor(
}
}
private fun handleCloseClick() {
private fun handleBackClicked() {
sendEvent(CompleteRegistrationEvent.NavigateBack)
}
@ -253,41 +266,14 @@ class CompleteRegistrationViewModel @Inject constructor(
mutableStateFlow.update { it.copy(confirmPasswordInput = action.input) }
}
private fun handleCreateAccountClick() = when {
state.userEmail.isBlank() -> {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required
.asText(R.string.email_address.asText()),
)
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
}
!state.userEmail.isValidEmail() -> {
private fun handleCallToActionClick() {
if (!state.userEmail.isValidEmail()) {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.invalid_email.asText(),
)
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
}
state.passwordInput.length < MIN_PASSWORD_LENGTH -> {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.master_password_length_val_message_x.asText(MIN_PASSWORD_LENGTH),
)
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
}
state.passwordInput != state.confirmPasswordInput -> {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.master_password_confirmation_val_message.asText(),
)
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
}
else -> {
} else {
submitRegisterAccountRequest(
shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
shouldIgnorePasswordStrength = false,
@ -295,6 +281,14 @@ class CompleteRegistrationViewModel @Inject constructor(
}
}
private fun handleMakePasswordStrongClickAction() {
sendEvent(CompleteRegistrationEvent.NavigateToMakePasswordStrong)
}
private fun handlePreventAccountLockoutClickAction() {
sendEvent(CompleteRegistrationEvent.NavigateToPreventAccountLockout)
}
private fun handleContinueWithBreachedPasswordClick() {
submitRegisterAccountRequest(
shouldCheckForDataBreaches = false,
@ -345,19 +339,18 @@ data class CompleteRegistrationState(
val isCheckDataBreachesToggled: Boolean,
val dialog: CompleteRegistrationDialog?,
val passwordStrengthState: PasswordStrengthState,
val onBoardingEnabled: Boolean,
) : Parcelable {
val passwordLengthLabel: Text
// Have to concat a few strings here, resulting string is:
// Important: Your master password cannot be recovered if you forget it! 12
// characters minimum
@Suppress("MaxLineLength")
get() = R.string.important.asText()
.concat(
": ".asText(),
R.string.your_master_password_cannot_be_recovered_if_you_forget_it_x_characters_minimum
.asText(MIN_PASSWORD_LENGTH),
)
/**
* The text to display on the call to action button.
*/
val callToActionText: Text
get() = if (onBoardingEnabled) {
R.string.next.asText()
} else {
R.string.create_account.asText()
}
/**
* Whether or not the provided master password is considered strong.
@ -374,6 +367,14 @@ data class CompleteRegistrationState(
PasswordStrengthState.STRONG,
-> true
}
/**
* Whether the form is valid.
*/
val hasValidMasterPassword: Boolean
get() = passwordInput == confirmPasswordInput &&
passwordInput.isNotBlank() &&
passwordInput.length >= MIN_PASSWORD_LENGTH
}
/**
@ -423,24 +424,33 @@ sealed class CompleteRegistrationEvent {
) : CompleteRegistrationEvent()
/**
* Navigates to the landing screen.
* Navigates to prevent account lockout info screen
*/
data object NavigateToLanding : CompleteRegistrationEvent()
data object NavigateToPreventAccountLockout : CompleteRegistrationEvent()
/**
* Navigates to make password strong screen
*/
data object NavigateToMakePasswordStrong : CompleteRegistrationEvent()
/**
* Navigates to the captcha verification screen.
*/
data class NavigateToLogin(
val email: String,
val captchaToken: String,
) : CompleteRegistrationEvent()
}
/**
* Models actions for the complete registration screen.
*/
sealed class CompleteRegistrationAction {
/**
* User clicked create account.
*/
data object CreateAccountClick : CompleteRegistrationAction()
/**
* User clicked close.
* User clicked back.
*/
data object CloseClick : CompleteRegistrationAction()
data object BackClick : CompleteRegistrationAction()
/**
* User clicked "Yes" when being asked if they are sure they want to use a breached password.
@ -472,6 +482,21 @@ sealed class CompleteRegistrationAction {
*/
data class CheckDataBreachesToggle(val newState: Boolean) : CompleteRegistrationAction()
/**
* User clicked on the make password strong card.
*/
data object MakePasswordStrongClick : CompleteRegistrationAction()
/**
* User clicked on learn to prevent lockout text.
*/
data object LearnToPreventLockoutClick : CompleteRegistrationAction()
/**
* User clicked on the "CTA" button.
*/
data object CallToActionClick : CompleteRegistrationAction()
/**
* Models actions that the [CompleteRegistrationViewModel] itself might send.
*/

View file

@ -0,0 +1,86 @@
package com.x8bit.bitwarden.ui.auth.feature.completeregistration.handlers
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationViewModel
/**
* Handler for the complete registration screen lambda invocations.
*/
@Suppress("LongParameterList")
class CompleteRegistrationHandler(
val onDismissErrorDialog: () -> Unit,
val onContinueWithBreachedPasswordClick: () -> Unit,
val onBackClick: () -> Unit,
val onPasswordInputChange: (String) -> Unit,
val onConfirmPasswordInputChange: (String) -> Unit,
val onPasswordHintChange: (String) -> Unit,
val onCheckDataBreachesToggle: (Boolean) -> Unit,
val onMakeStrongPassword: () -> Unit,
val onLearnToPreventLockout: () -> Unit,
val onCallToAction: () -> Unit,
) {
companion object {
/**
* Create [CompleteRegistrationHandler] with the given [viewModel] to send actions to.
*/
fun create(viewModel: CompleteRegistrationViewModel) = CompleteRegistrationHandler(
onDismissErrorDialog = {
viewModel.trySendAction(CompleteRegistrationAction.ErrorDialogDismiss)
},
onContinueWithBreachedPasswordClick = {
viewModel.trySendAction(
CompleteRegistrationAction.ContinueWithBreachedPasswordClick,
)
},
onBackClick = { viewModel.trySendAction(CompleteRegistrationAction.BackClick) },
onPasswordInputChange = {
viewModel.trySendAction(
CompleteRegistrationAction.PasswordInputChange(
it,
),
)
},
onConfirmPasswordInputChange = {
viewModel.trySendAction(
CompleteRegistrationAction.ConfirmPasswordInputChange(
it,
),
)
},
onPasswordHintChange = {
viewModel.trySendAction(
CompleteRegistrationAction.PasswordHintChange(
it,
),
)
},
onCheckDataBreachesToggle = {
viewModel.trySendAction(
CompleteRegistrationAction.CheckDataBreachesToggle(
it,
),
)
},
onMakeStrongPassword = {
viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick)
},
onLearnToPreventLockout = {
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
},
onCallToAction = {
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
},
)
}
}
/**
* Remember [CompleteRegistrationHandler] with the given [viewModel] within a [Composable] scope.
*/
@Composable
fun rememberCompleteRegistrationHandler(viewModel: CompleteRegistrationViewModel) =
remember(viewModel) {
CompleteRegistrationHandler.create(viewModel)
}

View file

@ -473,7 +473,7 @@ sealed class CreateAccountEvent {
data class NavigateToCaptcha(val uri: Uri) : CreateAccountEvent()
/**
* Navigates to the captcha verification screen.
* Navigates to the login screen bypassing captcha with token.
*/
data class NavigateToLogin(
val email: String,

View file

@ -10,13 +10,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -38,7 +34,9 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@ -89,7 +87,7 @@ fun MasterPasswordGuidanceScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(innerPadding)
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
) {
Column(
modifier = Modifier
@ -160,57 +158,26 @@ private fun TryGeneratorCard(
onCardClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(
onClick = onCardClicked,
shape = RoundedCornerShape(size = 16.dp),
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
BitwardenActionCard(
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
actionText = stringResource(
R.string.use_the_generator_to_create_a_strong_unique_password,
),
elevation = CardDefaults.elevatedCardElevation(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_generator),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp),
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(weight = 1f),
) {
Text(
text = stringResource(
R.string.use_the_generator_to_create_a_strong_unique_password,
),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.try_it_out),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
}
Spacer(modifier = Modifier.width(16.dp))
callToActionText = stringResource(R.string.try_it_out),
onCardClicked = onCardClicked,
modifier = modifier
.fillMaxWidth(),
trailingContent = {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_navigate_next),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.align(Alignment.CenterVertically)
.align(Alignment.Center)
.size(16.dp),
)
}
}
},
)
}
@Composable

View file

@ -0,0 +1,119 @@
package com.x8bit.bitwarden.ui.platform.components.card
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* A reusable card for displaying actions to the user.
*/
@Composable
fun BitwardenActionCard(
actionIcon: VectorPainter,
actionText: String,
callToActionText: String,
onCardClicked: () -> Unit,
modifier: Modifier = Modifier,
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
) {
Card(
onClick = onCardClicked,
shape = RoundedCornerShape(size = 16.dp),
modifier = modifier,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
),
elevation = CardDefaults.elevatedCardElevation(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Icon(
painter = actionIcon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp),
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(weight = 1f),
) {
Text(
text = actionText,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = callToActionText,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.align(Alignment.CenterVertically),
) {
trailingContent?.invoke(this)
}
}
}
}
@Preview
@Composable
private fun ActionCardPreview() {
BitwardenTheme {
BitwardenActionCard(
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
actionText = "This is an action.",
callToActionText = "Take action",
onCardClicked = { },
)
}
}
@Preview
@Composable
private fun ActionCardWithTrailingPreview() {
BitwardenTheme {
BitwardenActionCard(
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
actionText = "An action with trailing content",
callToActionText = "Take action",
onCardClicked = {},
trailingContent = {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_navigate_next),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
},
)
}
}

View file

@ -0,0 +1,66 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:pathData="M30.95,95.71C30.95,85.77 39.01,77.71 48.95,77.71H151.05C160.99,77.71 169.05,85.77 169.05,95.71V112.71H165.05V95.71C165.05,87.98 158.78,81.71 151.05,81.71H48.95C41.22,81.71 34.95,87.98 34.95,95.71V112.71H30.95V95.71ZM34.95,162.2V171.21C34.95,178.94 41.22,185.21 48.95,185.21H151.05C158.78,185.21 165.05,178.94 165.05,171.21V162.2H169.05V171.21C169.05,181.15 160.99,189.21 151.05,189.21H48.95C39.01,189.21 30.95,181.15 30.95,171.21V162.2H34.95Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M100.01,16.79C76.23,16.79 57.31,33.74 57.31,53.99V78.65H53.31V53.99C53.31,31.05 74.53,12.79 100.01,12.79C125.48,12.79 146.71,30.95 146.71,53.99V78.65H142.71V53.99C142.71,33.65 123.79,16.79 100.01,16.79Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M40.45,124.67C41.56,124.67 42.45,125.57 42.45,126.67V138.76C42.45,139.86 41.56,140.76 40.45,140.76C39.35,140.76 38.45,139.86 38.45,138.76V126.67C38.45,125.57 39.35,124.67 40.45,124.67Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M54,134.41C54.34,135.46 53.76,136.59 52.71,136.93L41.06,140.66C40.01,141 38.88,140.42 38.55,139.37C38.21,138.32 38.79,137.19 39.84,136.85L51.49,133.12C52.54,132.78 53.66,133.36 54,134.41Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M39.28,137.14C40.18,136.49 41.43,136.69 42.07,137.59L49.21,147.48C49.86,148.37 49.66,149.62 48.76,150.27C47.87,150.91 46.62,150.71 45.97,149.82L38.83,139.93C38.18,139.03 38.38,137.78 39.28,137.14Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M41.61,137.13C42.51,137.77 42.72,139.02 42.08,139.92L35.05,149.81C34.41,150.71 33.16,150.92 32.26,150.28C31.36,149.64 31.15,148.39 31.79,147.49L38.82,137.6C39.46,136.7 40.71,136.49 41.61,137.13Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M27.01,134.41C27.35,133.36 28.48,132.78 29.53,133.12L41.07,136.86C42.12,137.2 42.69,138.32 42.35,139.38C42.01,140.43 40.89,141 39.83,140.66L28.3,136.93C27.25,136.59 26.67,135.46 27.01,134.41Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M79.78,124.67C80.88,124.67 81.78,125.57 81.78,126.67V138.76C81.78,139.86 80.88,140.76 79.78,140.76C78.67,140.76 77.78,139.86 77.78,138.76V126.67C77.78,125.57 78.67,124.67 79.78,124.67Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M93.22,134.41C93.56,135.46 92.98,136.58 91.93,136.93L80.39,140.66C79.34,141 78.22,140.43 77.88,139.37C77.54,138.32 78.11,137.2 79.16,136.85L90.7,133.12C91.75,132.78 92.88,133.35 93.22,134.41Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M78.61,137.14C79.5,136.49 80.75,136.69 81.4,137.59L88.54,147.48C89.19,148.37 88.99,149.62 88.09,150.27C87.2,150.91 85.95,150.71 85.3,149.82L78.16,139.93C77.51,139.03 77.71,137.78 78.61,137.14Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M80.95,137.14C81.84,137.78 82.05,139.03 81.4,139.93L74.26,149.82C73.61,150.71 72.36,150.91 71.46,150.27C70.57,149.62 70.37,148.37 71.01,147.48L78.16,137.59C78.8,136.69 80.05,136.49 80.95,137.14Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M66.34,134.41C66.68,133.35 67.81,132.78 68.86,133.12L80.39,136.85C81.44,137.2 82.02,138.32 81.68,139.37C81.34,140.43 80.21,141 79.16,140.66L67.62,136.93C66.57,136.58 66,135.46 66.34,134.41Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M105.6,148.65C105.6,147.54 106.5,146.65 107.6,146.65H130.68C131.78,146.65 132.68,147.54 132.68,148.65C132.68,149.75 131.78,150.65 130.68,150.65H107.6C106.5,150.65 105.6,149.75 105.6,148.65Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M144.94,148.65C144.94,147.54 145.83,146.65 146.94,146.65H170.01C171.12,146.65 172.01,147.54 172.01,148.65C172.01,149.75 171.12,150.65 170.01,150.65H146.94C145.83,150.65 144.94,149.75 144.94,148.65Z"
android:fillColor="#E2E3E4"
android:fillType="evenOdd"/>
<path
android:pathData="M9.67,137.72C9.67,122.8 21.76,110.71 36.68,110.71H163.33C178.25,110.71 190.34,122.8 190.34,137.72C190.34,152.63 178.25,164.73 163.33,164.73H36.68C21.76,164.73 9.67,152.63 9.67,137.72ZM36.68,114.71C23.97,114.71 13.67,125.01 13.67,137.72C13.67,150.43 23.97,160.73 36.68,160.73H163.33C176.04,160.73 186.34,150.43 186.34,137.72C186.34,125.01 176.04,114.71 163.33,114.71H36.68Z"
android:fillColor="#6FD9E2"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,66 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:pathData="M30.95,95.71C30.95,85.77 39.01,77.71 48.95,77.71H151.05C160.99,77.71 169.05,85.77 169.05,95.71V112.71H165.05V95.71C165.05,87.98 158.78,81.71 151.05,81.71H48.95C41.22,81.71 34.95,87.98 34.95,95.71V112.71H30.95V95.71ZM34.95,162.2V171.21C34.95,178.94 41.22,185.21 48.95,185.21H151.05C158.78,185.21 165.05,178.94 165.05,171.21V162.2H169.05V171.21C169.05,181.15 160.99,189.21 151.05,189.21H48.95C39.01,189.21 30.95,181.15 30.95,171.21V162.2H34.95Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M100.01,16.79C76.23,16.79 57.31,33.74 57.31,53.99V78.65H53.31V53.99C53.31,31.05 74.53,12.79 100.01,12.79C125.48,12.79 146.71,30.95 146.71,53.99V78.65H142.71V53.99C142.71,33.65 123.79,16.79 100.01,16.79Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M40.45,124.67C41.56,124.67 42.45,125.57 42.45,126.67V138.76C42.45,139.86 41.56,140.76 40.45,140.76C39.35,140.76 38.45,139.86 38.45,138.76V126.67C38.45,125.57 39.35,124.67 40.45,124.67Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M54,134.41C54.34,135.46 53.76,136.59 52.71,136.93L41.06,140.66C40.01,141 38.88,140.42 38.55,139.37C38.21,138.32 38.79,137.19 39.84,136.85L51.49,133.12C52.54,132.78 53.66,133.36 54,134.41Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M39.28,137.14C40.18,136.49 41.43,136.69 42.07,137.59L49.21,147.48C49.86,148.37 49.66,149.62 48.76,150.27C47.87,150.91 46.62,150.71 45.97,149.82L38.83,139.93C38.18,139.03 38.38,137.78 39.28,137.14Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M41.61,137.13C42.51,137.77 42.72,139.02 42.08,139.92L35.05,149.81C34.41,150.71 33.16,150.92 32.26,150.28C31.36,149.64 31.15,148.39 31.79,147.49L38.82,137.6C39.46,136.7 40.71,136.49 41.61,137.13Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M27.01,134.41C27.35,133.36 28.48,132.78 29.53,133.12L41.07,136.86C42.12,137.2 42.69,138.32 42.35,139.38C42.01,140.43 40.89,141 39.83,140.66L28.3,136.93C27.25,136.59 26.67,135.46 27.01,134.41Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M79.78,124.67C80.88,124.67 81.78,125.57 81.78,126.67V138.76C81.78,139.86 80.88,140.76 79.78,140.76C78.67,140.76 77.78,139.86 77.78,138.76V126.67C77.78,125.57 78.67,124.67 79.78,124.67Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M93.22,134.41C93.56,135.46 92.98,136.59 91.93,136.93L80.39,140.66C79.34,141 78.22,140.43 77.88,139.38C77.54,138.32 78.11,137.2 79.16,136.86L90.7,133.12C91.75,132.78 92.88,133.36 93.22,134.41Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M78.61,137.14C79.5,136.49 80.75,136.69 81.4,137.59L88.54,147.48C89.19,148.37 88.99,149.62 88.09,150.27C87.2,150.91 85.95,150.71 85.3,149.82L78.16,139.93C77.51,139.03 77.71,137.78 78.61,137.14Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M80.95,137.14C81.84,137.78 82.05,139.03 81.4,139.93L74.26,149.82C73.61,150.71 72.36,150.91 71.46,150.27C70.57,149.62 70.37,148.37 71.01,147.48L78.16,137.59C78.8,136.69 80.05,136.49 80.95,137.14Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M66.34,134.41C66.68,133.36 67.81,132.78 68.86,133.12L80.39,136.86C81.44,137.2 82.02,138.32 81.68,139.38C81.34,140.43 80.21,141 79.16,140.66L67.62,136.93C66.57,136.59 66,135.46 66.34,134.41Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M105.6,148.65C105.6,147.54 106.5,146.65 107.6,146.65H130.68C131.78,146.65 132.68,147.54 132.68,148.65C132.68,149.75 131.78,150.65 130.68,150.65H107.6C106.5,150.65 105.6,149.75 105.6,148.65Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M144.94,148.65C144.94,147.54 145.83,146.65 146.94,146.65H170.01C171.12,146.65 172.01,147.54 172.01,148.65C172.01,149.75 171.12,150.65 170.01,150.65H146.94C145.83,150.65 144.94,149.75 144.94,148.65Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M9.67,137.72C9.67,122.8 21.76,110.71 36.68,110.71H163.33C178.25,110.71 190.34,122.8 190.34,137.72C190.34,152.63 178.25,164.73 163.33,164.73H36.68C21.76,164.73 9.67,152.63 9.67,137.72ZM36.68,114.71C23.97,114.71 13.67,125.01 13.67,137.72C13.67,150.43 23.97,160.73 36.68,160.73H163.33C176.04,160.73 186.34,150.43 186.34,137.72C186.34,125.01 176.04,114.71 163.33,114.71H36.68Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
</vector>

View file

@ -982,4 +982,9 @@ Do you want to switch to this account?</string>
<string name="email_address_required">Email address (required)</string>
<string name="select_the_link_in_the_email_to_verify_your_email_address_and_continue_creating_your_account">"Select the link in the email to verify your email address and continue creating your account. "</string>
<string name="change_email_address">Change email address</string>
<string name="next">Next</string>
<string name="bitwarden_cannot_recover_a_lost_or_forgotten_master_password">Bitwarden cannot recover a lost or forgotten master password.</string>
<string name="choose_your_master_password">Choose your master password</string>
<string name="choose_a_unique_and_strong_password_to_keep_your_information_safe">Choose a unique and strong password to keep your information safe.</string>
<string name="minimum_characters">%1$s characters</string>
</resources>

View file

@ -2,8 +2,12 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
@ -13,18 +17,16 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
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.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ContinueWithBreachedPasswordClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CreateAccountClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ErrorDialogDismiss
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordInputChange
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -35,16 +37,14 @@ import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.robolectric.annotation.Config
class CompleteRegistrationScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private var onNavigateToLandingCalled = false
private val intentManager = mockk<IntentManager>(relaxed = true) {
every { startCustomTabsActivity(any()) } just runs
every { startActivity(any()) } just runs
}
private var onNavigateToPreventAccountLockoutCalled = false
private var onNavigateToPasswordGuidanceCalled = false
private var onNavigateToLoginCalled = false
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<CompleteRegistrationEvent>()
@ -59,23 +59,58 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
composeTestRule.setContent {
CompleteRegistrationScreen(
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToLanding = { onNavigateToLandingCalled = true },
intentManager = intentManager,
onNavigateToPasswordGuidance = { onNavigateToPasswordGuidanceCalled = true },
onNavigateToPreventAccountLockout = {
onNavigateToPreventAccountLockoutCalled = true
},
onNavigateToLogin = { email, captchaToken ->
onNavigateToLoginCalled = true
assertTrue(email == EMAIL)
assertTrue(captchaToken == TOKEN)
},
viewModel = viewModel,
)
}
}
@Test
fun `app bar submit click should send CreateAccountClick action`() {
composeTestRule.onNodeWithText("Create account").performClick()
verify { viewModel.trySendAction(CreateAccountClick) }
fun `call to action with valid input click should send CreateAccountClick action`() {
mutableStateFlow.update {
it.copy(
passwordInput = "password1234",
confirmPasswordInput = "password1234",
)
}
composeTestRule
.onNode(hasText("Create account") and hasClickAction())
.performScrollTo()
.assertIsEnabled()
.performClick()
verify { viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) }
}
@Test
fun `call to action not enabled if passwords don't match`() {
mutableStateFlow.update {
it.copy(
passwordInput = "4321drowssap",
confirmPasswordInput = "password1234",
)
}
composeTestRule
.onNode(hasText("Create account") and hasClickAction())
.performScrollTo()
.assertIsNotEnabled()
.performClick()
verify(exactly = 0) {
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
}
}
@Test
fun `close click should send CloseClick action`() {
composeTestRule.onNodeWithContentDescription("Close").performClick()
verify { viewModel.trySendAction(CloseClick) }
composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(BackClick) }
}
@Test
@ -93,12 +128,6 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
assertTrue(onNavigateBackCalled)
}
@Test
fun `NavigateToLogin event should invoke navigate login lambda`() {
mutableEventFlow.tryEmit(CompleteRegistrationEvent.NavigateToLanding)
assertTrue(onNavigateToLandingCalled)
}
@Test
fun `password input change should send PasswordInputChange action`() {
composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT)
@ -182,7 +211,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_1)
}
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
composeTestRule.onNodeWithText("Weak").performScrollTo().assertIsDisplayed()
mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_2)
@ -211,6 +240,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
composeTestRule
.onAllNodesWithContentDescription("Show")
.assertCountEquals(2)[0]
.performScrollTo()
.performClick()
// after clicking there should be no Show buttons:
@ -222,6 +252,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
composeTestRule
.onAllNodesWithContentDescription("Hide")
.assertCountEquals(2)[1]
.performScrollTo()
.performClick()
// then there should be two show buttons again
@ -230,6 +261,80 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
.assertCountEquals(2)
}
@Test
fun `Click on action card should send MakePasswordStrongClick action`() {
composeTestRule
.onNodeWithText("Learn more")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick) }
}
@Test
fun `Click on prevent account lockout should send LearnToPreventLockoutClick action`() {
composeTestRule
.onNodeWithText("Learn about other ways to prevent account lockout")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
}
}
@Suppress("MaxLineLength")
@Test
fun `NavigateToPreventAccountLockout event should invoke navigate to prevent account lockout lambda`() {
mutableEventFlow.tryEmit(CompleteRegistrationEvent.NavigateToPreventAccountLockout)
assertTrue(onNavigateToPreventAccountLockoutCalled)
}
@Test
fun `NavigateToPasswordGuidance event should invoke navigate to password guidance lambda`() {
mutableEventFlow.tryEmit(CompleteRegistrationEvent.NavigateToMakePasswordStrong)
assertTrue(onNavigateToPasswordGuidanceCalled)
}
@Test
fun `Header should be displayed in portrait mode`() {
composeTestRule
.onNodeWithText("Choose your master password")
.performScrollTo()
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Choose a unique and strong password to keep your information safe.")
.performScrollTo()
.assertIsDisplayed()
}
@Config(qualifiers = "land")
@Test
fun `Header should be displayed in landscape mode`() {
composeTestRule
.onNodeWithText("Choose your master password")
.performScrollTo()
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Choose a unique and strong password to keep your information safe.")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun `NavigateToLogin event should invoke navigate to login lambda`() {
mutableEventFlow.tryEmit(
CompleteRegistrationEvent.NavigateToLogin(
email = EMAIL,
captchaToken = TOKEN,
),
)
assertTrue(onNavigateToLoginCalled)
}
companion object {
private const val EMAIL = "test@test.com"
private const val TOKEN = "token"
@ -244,6 +349,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
isCheckDataBreachesToggled = true,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
onBoardingEnabled = false,
)
}
}

View file

@ -13,12 +13,14 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
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.ui.auth.feature.completeregistration.CompleteRegistrationAction.CloseClick
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
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordHintChange
@ -28,12 +30,14 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@ -52,7 +56,9 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
private val specialCircumstanceManager: SpecialCircumstanceManager =
SpecialCircumstanceManagerImpl()
private var viewmodelVerifyEmailCalled = false
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
}
@BeforeEach
fun setUp() {
@ -86,13 +92,14 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
authRepository = mockAuthRepository,
environmentRepository = fakeEnvironmentRepository,
specialCircumstanceManager = specialCircumstanceManager,
featureFlagManager = featureFlagManager,
)
viewModel.onCleared()
assertTrue(specialCircumstanceManager.specialCircumstance == null)
}
@Test
fun `CreateAccountClick with password below 12 chars should show password length dialog`() =
fun `Password below 12 chars should have non-valid state`() =
runTest {
val input = "abcdefghikl"
coEvery {
@ -100,47 +107,24 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
} returns PasswordStrengthResult.Error
val viewModel = createCompleteRegistrationViewModel()
viewModel.trySendAction(PasswordInputChange(input))
val expectedState = DEFAULT_STATE.copy(
passwordInput = input,
dialog = CompleteRegistrationDialog.Error(
BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.master_password_length_val_message_x.asText(12),
),
),
)
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.stateFlow.test {
assertEquals(expectedState, awaitItem())
}
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
}
@Test
fun `CreateAccountClick with passwords not matching should show password match dialog`() =
fun `Passwords not matching should have non-valid state`() =
runTest {
coEvery {
mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD)
} returns PasswordStrengthResult.Error
val viewModel = createCompleteRegistrationViewModel()
viewModel.trySendAction(PasswordInputChange(PASSWORD))
val expectedState = DEFAULT_STATE.copy(
userEmail = EMAIL,
passwordInput = PASSWORD,
dialog = CompleteRegistrationDialog.Error(
BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.master_password_confirmation_val_message.asText(),
),
),
)
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.stateFlow.test {
assertEquals(expectedState, awaitItem())
}
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
}
@Test
fun `CreateAccountClick with all inputs valid should show and hide loading dialog`() = runTest {
fun `CallToActionClick with all inputs valid should show and hide loading dialog`() = runTest {
val repo = mockk<AuthRepository> {
coEvery {
register(
@ -159,13 +143,16 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
val stateFlow = viewModel.stateFlow.testIn(backgroundScope)
val eventFlow = viewModel.eventFlow.testIn(backgroundScope)
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem())
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
assertEquals(
VALID_INPUT_STATE.copy(dialog = CompleteRegistrationDialog.Loading),
stateFlow.awaitItem(),
)
assertEquals(
CompleteRegistrationEvent.NavigateToLanding,
CompleteRegistrationEvent.NavigateToLogin(
EMAIL,
CAPTCHA_BYPASS_TOKEN,
),
eventFlow.awaitItem(),
)
// Make sure loading dialog is hidden:
@ -174,7 +161,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
}
@Test
fun `CreateAccountClick register returns error should update errorDialogState`() = runTest {
fun `CallToActionClick register returns error should update errorDialogState`() = runTest {
val repo = mockk<AuthRepository> {
coEvery {
register(
@ -191,7 +178,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE, repo)
viewModel.stateFlow.test {
assertEquals(VALID_INPUT_STATE, awaitItem())
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
assertEquals(
VALID_INPUT_STATE.copy(dialog = CompleteRegistrationDialog.Loading),
awaitItem(),
@ -211,7 +198,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
}
@Test
fun `CreateAccountClick register returns Success should emit NavigateToLogin`() = runTest {
fun `CallToActionClick register returns Success should emit NavigateToLogin`() = runTest {
val repo = mockk<AuthRepository> {
coEvery {
register(
@ -227,9 +214,12 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
}
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE, repo)
viewModel.eventFlow.test {
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
assertEquals(
CompleteRegistrationEvent.NavigateToLanding,
CompleteRegistrationEvent.NavigateToLogin(
EMAIL,
CAPTCHA_BYPASS_TOKEN,
),
awaitItem(),
)
}
@ -266,7 +256,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
}
@Test
fun `CreateAccountClick register returns ShowDataBreaches should show HaveIBeenPwned dialog`() =
fun `CallToActionClick register returns ShowDataBreaches should show HaveIBeenPwned dialog`() =
runTest {
mockAuthRepository.apply {
coEvery {
@ -287,7 +277,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
val viewModel = createCompleteRegistrationViewModel(
completeRegistrationState = initialState,
)
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
viewModel.stateFlow.test {
assertEquals(
initialState.copy(
@ -304,7 +294,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
@Test
@Suppress("MaxLineLength")
fun `CreateAccountClick register returns DataBreachAndWeakPassword should show HaveIBeenPwned dialog`() =
fun `CallToActionClick register returns DataBreachAndWeakPassword should show HaveIBeenPwned dialog`() =
runTest {
mockAuthRepository.apply {
coEvery {
@ -326,7 +316,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
val viewModel =
createCompleteRegistrationViewModel(completeRegistrationState = initialState)
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
viewModel.stateFlow.test {
assertEquals(
initialState.copy(dialog = createHaveIBeenPwned()),
@ -337,7 +327,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
@Test
@Suppress("MaxLineLength")
fun `CreateAccountClick register returns WeakPassword should show HaveIBeenPwned dialog`() =
fun `CallToActionClick register returns WeakPassword should show HaveIBeenPwned dialog`() =
runTest {
mockAuthRepository.apply {
coEvery {
@ -359,7 +349,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
)
val viewModel =
createCompleteRegistrationViewModel(completeRegistrationState = initialState)
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
viewModel.stateFlow.test {
assertEquals(
initialState.copy(
@ -377,7 +367,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
fun `CloseClick should emit NavigateBack`() = runTest {
val viewModel = createCompleteRegistrationViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(CloseClick)
viewModel.trySendAction(BackClick)
assertEquals(CompleteRegistrationEvent.NavigateBack, awaitItem())
}
}
@ -498,15 +488,39 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `handling LearnToPreventLockoutClick should emit NavigateToPreventAccountLockout`() =
runTest {
val viewModel = createCompleteRegistrationViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
assertEquals(CompleteRegistrationEvent.NavigateToPreventAccountLockout, awaitItem())
}
}
@Test
fun `handling MakePasswordStrongClick should emit NavigateToMakePasswordStrong`() = runTest {
val viewModel = createCompleteRegistrationViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick)
assertEquals(CompleteRegistrationEvent.NavigateToMakePasswordStrong, awaitItem())
}
}
private fun createCompleteRegistrationViewModel(
completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE,
authRepository: AuthRepository = mockAuthRepository,
): CompleteRegistrationViewModel =
CompleteRegistrationViewModel(
savedStateHandle = SavedStateHandle(mapOf("state" to completeRegistrationState)),
savedStateHandle = SavedStateHandle(
mapOf(
"state" to completeRegistrationState,
),
),
authRepository = authRepository,
environmentRepository = fakeEnvironmentRepository,
specialCircumstanceManager = specialCircumstanceManager,
featureFlagManager = featureFlagManager,
)
companion object {
@ -524,6 +538,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
isCheckDataBreachesToggled = true,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
onBoardingEnabled = false,
)
private val VALID_INPUT_STATE = CompleteRegistrationState(
userEmail = EMAIL,
@ -535,6 +550,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
isCheckDataBreachesToggled = false,
dialog = null,
passwordStrengthState = PasswordStrengthState.GOOD,
onBoardingEnabled = false,
)
}
}