From 075956ce179cdaaaee117ac7193b2d513115c792 Mon Sep 17 00:00:00 2001
From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com>
Date: Wed, 21 Aug 2024 15:32:28 -0400
Subject: [PATCH] PM-10617 + PM-10637 update complete registration screen to
match new onboarding design (#3787)
---
.../ui/auth/feature/auth/AuthNavigation.kt | 17 +-
.../CompleteRegistrationNavigation.kt | 8 +-
.../CompleteRegistrationScreen.kt | 349 ++++++++++++------
.../CompleteRegistrationViewModel.kt | 139 ++++---
.../handlers/CompleteRegistrationHandler.kt | 86 +++++
.../createaccount/CreateAccountViewModel.kt | 2 +-
.../MasterPasswordGuidanceScreen.kt | 63 +---
.../components/card/BitwardenActionCard.kt | 119 ++++++
app/src/main/res/drawable-night/lock.xml | 66 ++++
app/src/main/res/drawable/lock.xml | 66 ++++
app/src/main/res/values/strings.xml | 5 +
.../CompleteRegistrationScreenTest.kt | 152 ++++++--
.../CompleteRegistrationViewModelTest.kt | 110 +++---
13 files changed, 883 insertions(+), 299 deletions(-)
create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/handlers/CompleteRegistrationHandler.kt
create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt
create mode 100644 app/src/main/res/drawable-night/lock.xml
create mode 100644 app/src/main/res/drawable/lock.xml
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt
index 6ac313d6d..119f9453d 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt
@@ -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(
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt
index b4b61094b..c4ff47807 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt
@@ -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,
)
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt
index 90acbab82..309b6fbf0 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt
@@ -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(),
+ )
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt
index f1024702a..f925ed212 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt
@@ -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.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/handlers/CompleteRegistrationHandler.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/handlers/CompleteRegistrationHandler.kt
new file mode 100644
index 000000000..2522f9a26
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/handlers/CompleteRegistrationHandler.kt
@@ -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)
+ }
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModel.kt
index 5d6e5b4ed..e8a313284 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModel.kt
@@ -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,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt
index e8913541a..1e55a0de6 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt
@@ -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
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt
new file mode 100644
index 000000000..a0296cb19
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt
@@ -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,
+ )
+ },
+ )
+ }
+}
diff --git a/app/src/main/res/drawable-night/lock.xml b/app/src/main/res/drawable-night/lock.xml
new file mode 100644
index 000000000..7a8422080
--- /dev/null
+++ b/app/src/main/res/drawable-night/lock.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/lock.xml b/app/src/main/res/drawable/lock.xml
new file mode 100644
index 000000000..b409a4304
--- /dev/null
+++ b/app/src/main/res/drawable/lock.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f21e3176b..36a1ec9a6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -982,4 +982,9 @@ Do you want to switch to this account?
Email address (required)
"Select the link in the email to verify your email address and continue creating your account. "
Change email address
+ Next
+ Bitwarden cannot recover a lost or forgotten master password.
+ Choose your master password
+ Choose a unique and strong password to keep your information safe.
+ %1$s characters
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt
index e28bb40bc..59fe6ab18 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt
@@ -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(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()
@@ -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,
)
}
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt
index 15ebb452c..617c1a313 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt
@@ -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(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 {
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 {
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 {
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,
)
}
}