mirror of
https://github.com/bitwarden/android.git
synced 2024-12-18 15:21:53 +03:00
[PM-11270] hide new UI in complete registration screen behind flag pt. 2 (#3812)
This commit is contained in:
parent
b7330392cc
commit
9db09c18cc
4 changed files with 394 additions and 103 deletions
|
@ -46,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
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.appbar.BitwardenTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
|
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.BitwardenBasicDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||||
|
@ -135,11 +136,24 @@ fun CompleteRegistrationScreen(
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
BitwardenTopAppBar(
|
BitwardenTopAppBar(
|
||||||
title = stringResource(id = R.string.create_account),
|
title = if (state.onboardingEnabled) {
|
||||||
|
stringResource(id = R.string.create_account)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.set_password)
|
||||||
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
|
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
|
||||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||||
onNavigationIconClick = handler.onBackClick,
|
onNavigationIconClick = handler.onBackClick,
|
||||||
|
actions = {
|
||||||
|
if (!state.onboardingEnabled) {
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = state.callToActionText(),
|
||||||
|
onClick = handler.onCallToAction,
|
||||||
|
modifier = Modifier.testTag("CreateAccountButton"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
|
@ -157,10 +171,11 @@ fun CompleteRegistrationScreen(
|
||||||
passwordHintInput = state.passwordHintInput,
|
passwordHintInput = state.passwordHintInput,
|
||||||
isCheckDataBreachesToggled = state.isCheckDataBreachesToggled,
|
isCheckDataBreachesToggled = state.isCheckDataBreachesToggled,
|
||||||
handler = handler,
|
handler = handler,
|
||||||
modifier = Modifier.standardHorizontalMargin(),
|
nextButtonEnabled = state.validSubmissionReady,
|
||||||
nextButtonEnabled = state.hasValidMasterPassword,
|
|
||||||
callToActionText = state.callToActionText(),
|
callToActionText = state.callToActionText(),
|
||||||
minimumPasswordLength = state.minimumPasswordLength,
|
minimumPasswordLength = state.minimumPasswordLength,
|
||||||
|
showNewOnboardingUi = state.onboardingEnabled,
|
||||||
|
userEmail = state.userEmail,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
}
|
}
|
||||||
|
@ -170,6 +185,7 @@ fun CompleteRegistrationScreen(
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
private fun CompleteRegistrationContent(
|
private fun CompleteRegistrationContent(
|
||||||
|
userEmail: String,
|
||||||
passwordInput: String,
|
passwordInput: String,
|
||||||
passwordStrengthState: PasswordStrengthState,
|
passwordStrengthState: PasswordStrengthState,
|
||||||
confirmPasswordInput: String,
|
confirmPasswordInput: String,
|
||||||
|
@ -179,6 +195,7 @@ private fun CompleteRegistrationContent(
|
||||||
minimumPasswordLength: Int,
|
minimumPasswordLength: Int,
|
||||||
callToActionText: String,
|
callToActionText: String,
|
||||||
handler: CompleteRegistrationHandler,
|
handler: CompleteRegistrationHandler,
|
||||||
|
showNewOnboardingUi: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
@ -186,8 +203,11 @@ private fun CompleteRegistrationContent(
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
if (showNewOnboardingUi) {
|
||||||
CompleteRegistrationContentHeader(
|
CompleteRegistrationContentHeader(
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
BitwardenActionCard(
|
BitwardenActionCard(
|
||||||
|
@ -195,8 +215,17 @@ private fun CompleteRegistrationContent(
|
||||||
actionText = stringResource(id = R.string.what_makes_a_password_strong),
|
actionText = stringResource(id = R.string.what_makes_a_password_strong),
|
||||||
callToActionText = stringResource(id = R.string.learn_more),
|
callToActionText = stringResource(id = R.string.learn_more),
|
||||||
onCardClicked = handler.onMakeStrongPassword,
|
onCardClicked = handler.onMakeStrongPassword,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
LegacyHeaderContent(
|
||||||
|
userEmail = userEmail,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
var showPassword by rememberSaveable { mutableStateOf(false) }
|
var showPassword by rememberSaveable { mutableStateOf(false) }
|
||||||
|
@ -208,7 +237,8 @@ private fun CompleteRegistrationContent(
|
||||||
onValueChange = handler.onPasswordInputChange,
|
onValueChange = handler.onPasswordInputChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("MasterPasswordEntry")
|
.testTag("MasterPasswordEntry")
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
showPasswordTestTag = "PasswordVisibilityToggle",
|
||||||
imeAction = ImeAction.Next,
|
imeAction = ImeAction.Next,
|
||||||
)
|
)
|
||||||
|
@ -216,7 +246,8 @@ private fun CompleteRegistrationContent(
|
||||||
PasswordStrengthIndicator(
|
PasswordStrengthIndicator(
|
||||||
state = passwordStrengthState,
|
state = passwordStrengthState,
|
||||||
currentCharacterCount = passwordInput.length,
|
currentCharacterCount = passwordInput.length,
|
||||||
minimumCharacterCount = minimumPasswordLength,
|
minimumCharacterCount = minimumPasswordLength.takeIf { showNewOnboardingUi },
|
||||||
|
modifier = Modifier.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
BitwardenPasswordField(
|
BitwardenPasswordField(
|
||||||
|
@ -227,7 +258,8 @@ private fun CompleteRegistrationContent(
|
||||||
onValueChange = handler.onConfirmPasswordInputChange,
|
onValueChange = handler.onConfirmPasswordInputChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("ConfirmMasterPasswordEntry")
|
.testTag("ConfirmMasterPasswordEntry")
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
|
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
@ -235,34 +267,50 @@ private fun CompleteRegistrationContent(
|
||||||
label = stringResource(id = R.string.master_password_hint),
|
label = stringResource(id = R.string.master_password_hint),
|
||||||
value = passwordHintInput,
|
value = passwordHintInput,
|
||||||
onValueChange = handler.onPasswordHintChange,
|
onValueChange = handler.onPasswordHintChange,
|
||||||
hint = stringResource(
|
hint = if (showNewOnboardingUi) {
|
||||||
|
stringResource(
|
||||||
R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password,
|
R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password,
|
||||||
),
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.master_password_description)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("MasterPasswordHintLabel")
|
.testTag("MasterPasswordHintLabel")
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
|
if (showNewOnboardingUi) {
|
||||||
BitwardenClickableText(
|
BitwardenClickableText(
|
||||||
label = stringResource(id = R.string.learn_about_other_ways_to_prevent_account_lockout),
|
label = stringResource(
|
||||||
|
id = R.string.learn_about_other_ways_to_prevent_account_lockout,
|
||||||
|
),
|
||||||
onClick = handler.onLearnToPreventLockout,
|
onClick = handler.onLearnToPreventLockout,
|
||||||
style = nonMaterialTypography.labelMediumProminent,
|
style = nonMaterialTypography.labelMediumProminent,
|
||||||
|
modifier = Modifier.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
BitwardenSwitch(
|
BitwardenSwitch(
|
||||||
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
|
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
|
||||||
isChecked = isCheckDataBreachesToggled,
|
isChecked = isCheckDataBreachesToggled,
|
||||||
onCheckedChange = handler.onCheckDataBreachesToggle,
|
onCheckedChange = handler.onCheckDataBreachesToggle,
|
||||||
modifier = Modifier.testTag("CheckExposedMasterPasswordToggle"),
|
modifier = Modifier
|
||||||
|
.testTag("CheckExposedMasterPasswordToggle")
|
||||||
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
|
if (showNewOnboardingUi) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
BitwardenFilledButton(
|
BitwardenFilledButton(
|
||||||
label = callToActionText,
|
label = callToActionText,
|
||||||
isEnabled = nextButtonEnabled,
|
isEnabled = nextButtonEnabled,
|
||||||
onClick = handler.onCallToAction,
|
onClick = handler.onCallToAction,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CompleteRegistrationContentHeader(
|
private fun CompleteRegistrationContentHeader(
|
||||||
|
@ -286,6 +334,24 @@ private fun CompleteRegistrationContentHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LegacyHeaderContent(
|
||||||
|
userEmail: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account,
|
||||||
|
userEmail,
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header content ordered with the image "first" and the text "second" which can be placed in a
|
* Header content ordered with the image "first" and the text "second" which can be placed in a
|
||||||
* [Column] or [Row].
|
* [Column] or [Row].
|
||||||
|
@ -321,7 +387,7 @@ private fun OrderedHeaderContent() {
|
||||||
|
|
||||||
@PreviewScreenSizes
|
@PreviewScreenSizes
|
||||||
@Composable
|
@Composable
|
||||||
private fun CompleteRegistrationContent_preview() {
|
private fun CompleteRegistrationContentOldUI_preview() {
|
||||||
BitwardenTheme {
|
BitwardenTheme {
|
||||||
CompleteRegistrationContent(
|
CompleteRegistrationContent(
|
||||||
passwordInput = "tortor",
|
passwordInput = "tortor",
|
||||||
|
@ -345,6 +411,40 @@ private fun CompleteRegistrationContent_preview() {
|
||||||
nextButtonEnabled = true,
|
nextButtonEnabled = true,
|
||||||
modifier = Modifier.standardHorizontalMargin(),
|
modifier = Modifier.standardHorizontalMargin(),
|
||||||
minimumPasswordLength = 12,
|
minimumPasswordLength = 12,
|
||||||
|
showNewOnboardingUi = false,
|
||||||
|
userEmail = "fake@email.com",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewScreenSizes
|
||||||
|
@Composable
|
||||||
|
private fun CompleteRegistrationContentNewUI_preview() {
|
||||||
|
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(),
|
||||||
|
minimumPasswordLength = 12,
|
||||||
|
showNewOnboardingUi = true,
|
||||||
|
userEmail = "fake@email.com",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -64,7 +65,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||||
isCheckDataBreachesToggled = true,
|
isCheckDataBreachesToggled = true,
|
||||||
dialog = null,
|
dialog = null,
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
passwordStrengthState = PasswordStrengthState.NONE,
|
||||||
onBoardingEnabled = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow),
|
onboardingEnabled = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow),
|
||||||
minimumPasswordLength = MIN_PASSWORD_LENGTH,
|
minimumPasswordLength = MIN_PASSWORD_LENGTH,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -82,6 +83,14 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||||
stateFlow
|
stateFlow
|
||||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
featureFlagManager
|
||||||
|
.getFeatureFlagFlow(FlagKey.OnboardingFlow)
|
||||||
|
.map {
|
||||||
|
Internal.UpdateOnboardingFeatureState(newValue = it)
|
||||||
|
}
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@ -114,6 +123,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
CompleteRegistrationAction.CallToActionClick -> handleCallToActionClick()
|
CompleteRegistrationAction.CallToActionClick -> handleCallToActionClick()
|
||||||
|
is Internal.UpdateOnboardingFeatureState -> handleUpdateOnboardingFeatureState(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +141,12 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateOnboardingFeatureState(action: Internal.UpdateOnboardingFeatureState) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(onboardingEnabled = action.newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handlePasswordStrengthResult(action: ReceivePasswordStrengthResult) {
|
private fun handlePasswordStrengthResult(action: ReceivePasswordStrengthResult) {
|
||||||
when (val result = action.result) {
|
when (val result = action.result) {
|
||||||
is PasswordStrengthResult.Success -> {
|
is PasswordStrengthResult.Success -> {
|
||||||
|
@ -267,14 +283,41 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||||
mutableStateFlow.update { it.copy(confirmPasswordInput = action.input) }
|
mutableStateFlow.update { it.copy(confirmPasswordInput = action.input) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCallToActionClick() {
|
private fun handleCallToActionClick() = when {
|
||||||
if (!state.userEmail.isValidEmail()) {
|
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() -> {
|
||||||
val dialog = BasicDialogState.Shown(
|
val dialog = BasicDialogState.Shown(
|
||||||
title = R.string.an_error_has_occurred.asText(),
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
message = R.string.invalid_email.asText(),
|
message = R.string.invalid_email.asText(),
|
||||||
)
|
)
|
||||||
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
|
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
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 -> {
|
||||||
submitRegisterAccountRequest(
|
submitRegisterAccountRequest(
|
||||||
shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
|
shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
|
||||||
shouldIgnorePasswordStrength = false,
|
shouldIgnorePasswordStrength = false,
|
||||||
|
@ -340,7 +383,7 @@ data class CompleteRegistrationState(
|
||||||
val isCheckDataBreachesToggled: Boolean,
|
val isCheckDataBreachesToggled: Boolean,
|
||||||
val dialog: CompleteRegistrationDialog?,
|
val dialog: CompleteRegistrationDialog?,
|
||||||
val passwordStrengthState: PasswordStrengthState,
|
val passwordStrengthState: PasswordStrengthState,
|
||||||
val onBoardingEnabled: Boolean,
|
val onboardingEnabled: Boolean,
|
||||||
val minimumPasswordLength: Int,
|
val minimumPasswordLength: Int,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
@ -348,7 +391,7 @@ data class CompleteRegistrationState(
|
||||||
* The text to display on the call to action button.
|
* The text to display on the call to action button.
|
||||||
*/
|
*/
|
||||||
val callToActionText: Text
|
val callToActionText: Text
|
||||||
get() = if (onBoardingEnabled) {
|
get() = if (onboardingEnabled) {
|
||||||
R.string.next.asText()
|
R.string.next.asText()
|
||||||
} else {
|
} else {
|
||||||
R.string.create_account.asText()
|
R.string.create_account.asText()
|
||||||
|
@ -373,10 +416,10 @@ data class CompleteRegistrationState(
|
||||||
/**
|
/**
|
||||||
* Whether the form is valid.
|
* Whether the form is valid.
|
||||||
*/
|
*/
|
||||||
val hasValidMasterPassword: Boolean
|
val validSubmissionReady: Boolean
|
||||||
get() = passwordInput == confirmPasswordInput &&
|
get() = passwordInput.isNotBlank() &&
|
||||||
passwordInput.isNotBlank() &&
|
confirmPasswordInput.isNotBlank() &&
|
||||||
passwordInput.length >= MIN_PASSWORD_LENGTH
|
passwordInput.length >= minimumPasswordLength
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -516,5 +559,10 @@ sealed class CompleteRegistrationAction {
|
||||||
data class ReceivePasswordStrengthResult(
|
data class ReceivePasswordStrengthResult(
|
||||||
val result: PasswordStrengthResult,
|
val result: PasswordStrengthResult,
|
||||||
) : Internal()
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate on boarding feature state has been updated.
|
||||||
|
*/
|
||||||
|
data class UpdateOnboardingFeatureState(val newValue: Boolean) : Internal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,17 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `determine if using the old ui by title text`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Set password")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNode(hasText("Create account") and !hasClickAction())
|
||||||
|
.assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `call to action with valid input click should send CreateAccountClick action`() {
|
fun `call to action with valid input click should send CreateAccountClick action`() {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
|
@ -83,30 +94,11 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNode(hasText("Create account") and hasClickAction())
|
.onNode(hasText("Create account") and hasClickAction())
|
||||||
.performScrollTo()
|
|
||||||
.assertIsEnabled()
|
.assertIsEnabled()
|
||||||
.performClick()
|
.performClick()
|
||||||
verify { viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) }
|
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
|
@Test
|
||||||
fun `close click should send CloseClick action`() {
|
fun `close click should send CloseClick action`() {
|
||||||
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||||
|
@ -261,28 +253,6 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||||
.assertCountEquals(2)
|
.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")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToPreventAccountLockout event should invoke navigate to prevent account lockout lambda`() {
|
fun `NavigateToPreventAccountLockout event should invoke navigate to prevent account lockout lambda`() {
|
||||||
|
@ -297,7 +267,90 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Header should be displayed in portrait mode`() {
|
fun `NavigateToLogin event should invoke navigate to login lambda`() {
|
||||||
|
mutableEventFlow.tryEmit(
|
||||||
|
CompleteRegistrationEvent.NavigateToLogin(
|
||||||
|
email = EMAIL,
|
||||||
|
captchaToken = TOKEN,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(onNavigateToLoginCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Onboarding UI tests
|
||||||
|
@Test
|
||||||
|
fun `determine if using the new ui by title text`() = testWithFeatureFlagOn {
|
||||||
|
composeTestRule
|
||||||
|
.onNode(hasText("Create account") and !hasClickAction())
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Set password")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `call to action state should update with input based on if both fields are populated`() =
|
||||||
|
testWithFeatureFlagOn {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
passwordInput = "",
|
||||||
|
confirmPasswordInput = "password1234",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Next")
|
||||||
|
.assertIsNotEnabled()
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify(exactly = 0) {
|
||||||
|
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
passwordInput = "password1234",
|
||||||
|
confirmPasswordInput = "password1234",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Next")
|
||||||
|
.assertIsEnabled()
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Click on action card should send MakePasswordStrongClick action`() =
|
||||||
|
testWithFeatureFlagOn {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Learn more")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify { viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Click on prevent account lockout should send LearnToPreventLockoutClick action`() =
|
||||||
|
testWithFeatureFlagOn {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Learn about other ways to prevent account lockout")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Header should be displayed in portrait mode`() = testWithFeatureFlagOn {
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Choose your master password")
|
.onNodeWithText("Choose your master password")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -311,7 +364,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Config(qualifiers = "land")
|
@Config(qualifiers = "land")
|
||||||
@Test
|
@Test
|
||||||
fun `Header should be displayed in landscape mode`() {
|
fun `Header should be displayed in landscape mode`() = testWithFeatureFlagOn {
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Choose your master password")
|
.onNodeWithText("Choose your master password")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -323,16 +376,22 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private fun testWithFeatureFlagOn(test: () -> Unit) {
|
||||||
fun `NavigateToLogin event should invoke navigate to login lambda`() {
|
turnFeatureFlagOn()
|
||||||
mutableEventFlow.tryEmit(
|
test()
|
||||||
CompleteRegistrationEvent.NavigateToLogin(
|
turnFeatureFlagOff()
|
||||||
email = EMAIL,
|
}
|
||||||
captchaToken = TOKEN,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assertTrue(onNavigateToLoginCalled)
|
private fun turnFeatureFlagOn() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(onboardingEnabled = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun turnFeatureFlagOff() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(onboardingEnabled = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -349,7 +408,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||||
isCheckDataBreachesToggled = true,
|
isCheckDataBreachesToggled = true,
|
||||||
dialog = null,
|
dialog = null,
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
passwordStrengthState = PasswordStrengthState.NONE,
|
||||||
onBoardingEnabled = false,
|
onboardingEnabled = false,
|
||||||
minimumPasswordLength = 12,
|
minimumPasswordLength = 12,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.unmockkStatic
|
import io.mockk.unmockkStatic
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
@ -55,9 +56,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
private val specialCircumstanceManager: SpecialCircumstanceManager =
|
private val specialCircumstanceManager: SpecialCircumstanceManager =
|
||||||
SpecialCircumstanceManagerImpl()
|
SpecialCircumstanceManagerImpl()
|
||||||
|
private val mutableFeatureFlagFlow = MutableStateFlow(false)
|
||||||
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
||||||
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
|
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
|
||||||
|
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -108,7 +110,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||||
val viewModel = createCompleteRegistrationViewModel()
|
val viewModel = createCompleteRegistrationViewModel()
|
||||||
viewModel.trySendAction(PasswordInputChange(input))
|
viewModel.trySendAction(PasswordInputChange(input))
|
||||||
|
|
||||||
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
|
assertFalse(viewModel.stateFlow.value.validSubmissionReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -120,7 +122,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||||
val viewModel = createCompleteRegistrationViewModel()
|
val viewModel = createCompleteRegistrationViewModel()
|
||||||
viewModel.trySendAction(PasswordInputChange(PASSWORD))
|
viewModel.trySendAction(PasswordInputChange(PASSWORD))
|
||||||
|
|
||||||
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
|
assertFalse(viewModel.stateFlow.value.validSubmissionReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -507,6 +509,88 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `feature flag state update is captured in ViewModel state`() {
|
||||||
|
mutableFeatureFlagFlow.value = true
|
||||||
|
val viewModel = createCompleteRegistrationViewModel()
|
||||||
|
assertTrue(viewModel.stateFlow.value.onboardingEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CreateAccountClick with password below 12 chars should show password length dialog`() =
|
||||||
|
runTest {
|
||||||
|
val input = "abcdefghikl"
|
||||||
|
coEvery {
|
||||||
|
mockAuthRepository.getPasswordStrength(EMAIL, input)
|
||||||
|
} 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.CallToActionClick)
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(expectedState, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CreateAccountClick with passwords not matching should show password match dialog`() =
|
||||||
|
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.CallToActionClick)
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(expectedState, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CreateAccountClick with no email not should show dialog`() =
|
||||||
|
runTest {
|
||||||
|
coEvery {
|
||||||
|
mockAuthRepository.getPasswordStrength("", PASSWORD)
|
||||||
|
} returns PasswordStrengthResult.Error
|
||||||
|
val viewModel = createCompleteRegistrationViewModel(
|
||||||
|
DEFAULT_STATE.copy(userEmail = ""),
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(PasswordInputChange(PASSWORD))
|
||||||
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
|
userEmail = "",
|
||||||
|
passwordInput = PASSWORD,
|
||||||
|
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()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(expectedState, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createCompleteRegistrationViewModel(
|
private fun createCompleteRegistrationViewModel(
|
||||||
completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE,
|
completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE,
|
||||||
authRepository: AuthRepository = mockAuthRepository,
|
authRepository: AuthRepository = mockAuthRepository,
|
||||||
|
@ -538,7 +622,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||||
isCheckDataBreachesToggled = true,
|
isCheckDataBreachesToggled = true,
|
||||||
dialog = null,
|
dialog = null,
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
passwordStrengthState = PasswordStrengthState.NONE,
|
||||||
onBoardingEnabled = false,
|
onboardingEnabled = false,
|
||||||
minimumPasswordLength = 12,
|
minimumPasswordLength = 12,
|
||||||
)
|
)
|
||||||
private val VALID_INPUT_STATE = CompleteRegistrationState(
|
private val VALID_INPUT_STATE = CompleteRegistrationState(
|
||||||
|
@ -551,7 +635,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||||
isCheckDataBreachesToggled = false,
|
isCheckDataBreachesToggled = false,
|
||||||
dialog = null,
|
dialog = null,
|
||||||
passwordStrengthState = PasswordStrengthState.GOOD,
|
passwordStrengthState = PasswordStrengthState.GOOD,
|
||||||
onBoardingEnabled = false,
|
onboardingEnabled = false,
|
||||||
minimumPasswordLength = 12,
|
minimumPasswordLength = 12,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue