mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
PM-10617 + PM-10637 update complete registration screen to match new onboarding design (#3787)
This commit is contained in:
parent
13b256d4e9
commit
075956ce17
13 changed files with 883 additions and 299 deletions
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
66
app/src/main/res/drawable-night/lock.xml
Normal file
66
app/src/main/res/drawable-night/lock.xml
Normal file
|
@ -0,0 +1,66 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="200">
|
||||
<path
|
||||
android:pathData="M30.95,95.71C30.95,85.77 39.01,77.71 48.95,77.71H151.05C160.99,77.71 169.05,85.77 169.05,95.71V112.71H165.05V95.71C165.05,87.98 158.78,81.71 151.05,81.71H48.95C41.22,81.71 34.95,87.98 34.95,95.71V112.71H30.95V95.71ZM34.95,162.2V171.21C34.95,178.94 41.22,185.21 48.95,185.21H151.05C158.78,185.21 165.05,178.94 165.05,171.21V162.2H169.05V171.21C169.05,181.15 160.99,189.21 151.05,189.21H48.95C39.01,189.21 30.95,181.15 30.95,171.21V162.2H34.95Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M100.01,16.79C76.23,16.79 57.31,33.74 57.31,53.99V78.65H53.31V53.99C53.31,31.05 74.53,12.79 100.01,12.79C125.48,12.79 146.71,30.95 146.71,53.99V78.65H142.71V53.99C142.71,33.65 123.79,16.79 100.01,16.79Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M40.45,124.67C41.56,124.67 42.45,125.57 42.45,126.67V138.76C42.45,139.86 41.56,140.76 40.45,140.76C39.35,140.76 38.45,139.86 38.45,138.76V126.67C38.45,125.57 39.35,124.67 40.45,124.67Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M54,134.41C54.34,135.46 53.76,136.59 52.71,136.93L41.06,140.66C40.01,141 38.88,140.42 38.55,139.37C38.21,138.32 38.79,137.19 39.84,136.85L51.49,133.12C52.54,132.78 53.66,133.36 54,134.41Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M39.28,137.14C40.18,136.49 41.43,136.69 42.07,137.59L49.21,147.48C49.86,148.37 49.66,149.62 48.76,150.27C47.87,150.91 46.62,150.71 45.97,149.82L38.83,139.93C38.18,139.03 38.38,137.78 39.28,137.14Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M41.61,137.13C42.51,137.77 42.72,139.02 42.08,139.92L35.05,149.81C34.41,150.71 33.16,150.92 32.26,150.28C31.36,149.64 31.15,148.39 31.79,147.49L38.82,137.6C39.46,136.7 40.71,136.49 41.61,137.13Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M27.01,134.41C27.35,133.36 28.48,132.78 29.53,133.12L41.07,136.86C42.12,137.2 42.69,138.32 42.35,139.38C42.01,140.43 40.89,141 39.83,140.66L28.3,136.93C27.25,136.59 26.67,135.46 27.01,134.41Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M79.78,124.67C80.88,124.67 81.78,125.57 81.78,126.67V138.76C81.78,139.86 80.88,140.76 79.78,140.76C78.67,140.76 77.78,139.86 77.78,138.76V126.67C77.78,125.57 78.67,124.67 79.78,124.67Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M93.22,134.41C93.56,135.46 92.98,136.58 91.93,136.93L80.39,140.66C79.34,141 78.22,140.43 77.88,139.37C77.54,138.32 78.11,137.2 79.16,136.85L90.7,133.12C91.75,132.78 92.88,133.35 93.22,134.41Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M78.61,137.14C79.5,136.49 80.75,136.69 81.4,137.59L88.54,147.48C89.19,148.37 88.99,149.62 88.09,150.27C87.2,150.91 85.95,150.71 85.3,149.82L78.16,139.93C77.51,139.03 77.71,137.78 78.61,137.14Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M80.95,137.14C81.84,137.78 82.05,139.03 81.4,139.93L74.26,149.82C73.61,150.71 72.36,150.91 71.46,150.27C70.57,149.62 70.37,148.37 71.01,147.48L78.16,137.59C78.8,136.69 80.05,136.49 80.95,137.14Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M66.34,134.41C66.68,133.35 67.81,132.78 68.86,133.12L80.39,136.85C81.44,137.2 82.02,138.32 81.68,139.37C81.34,140.43 80.21,141 79.16,140.66L67.62,136.93C66.57,136.58 66,135.46 66.34,134.41Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M105.6,148.65C105.6,147.54 106.5,146.65 107.6,146.65H130.68C131.78,146.65 132.68,147.54 132.68,148.65C132.68,149.75 131.78,150.65 130.68,150.65H107.6C106.5,150.65 105.6,149.75 105.6,148.65Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M144.94,148.65C144.94,147.54 145.83,146.65 146.94,146.65H170.01C171.12,146.65 172.01,147.54 172.01,148.65C172.01,149.75 171.12,150.65 170.01,150.65H146.94C145.83,150.65 144.94,149.75 144.94,148.65Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M9.67,137.72C9.67,122.8 21.76,110.71 36.68,110.71H163.33C178.25,110.71 190.34,122.8 190.34,137.72C190.34,152.63 178.25,164.73 163.33,164.73H36.68C21.76,164.73 9.67,152.63 9.67,137.72ZM36.68,114.71C23.97,114.71 13.67,125.01 13.67,137.72C13.67,150.43 23.97,160.73 36.68,160.73H163.33C176.04,160.73 186.34,150.43 186.34,137.72C186.34,125.01 176.04,114.71 163.33,114.71H36.68Z"
|
||||
android:fillColor="#6FD9E2"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
66
app/src/main/res/drawable/lock.xml
Normal file
66
app/src/main/res/drawable/lock.xml
Normal file
|
@ -0,0 +1,66 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="200">
|
||||
<path
|
||||
android:pathData="M30.95,95.71C30.95,85.77 39.01,77.71 48.95,77.71H151.05C160.99,77.71 169.05,85.77 169.05,95.71V112.71H165.05V95.71C165.05,87.98 158.78,81.71 151.05,81.71H48.95C41.22,81.71 34.95,87.98 34.95,95.71V112.71H30.95V95.71ZM34.95,162.2V171.21C34.95,178.94 41.22,185.21 48.95,185.21H151.05C158.78,185.21 165.05,178.94 165.05,171.21V162.2H169.05V171.21C169.05,181.15 160.99,189.21 151.05,189.21H48.95C39.01,189.21 30.95,181.15 30.95,171.21V162.2H34.95Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M100.01,16.79C76.23,16.79 57.31,33.74 57.31,53.99V78.65H53.31V53.99C53.31,31.05 74.53,12.79 100.01,12.79C125.48,12.79 146.71,30.95 146.71,53.99V78.65H142.71V53.99C142.71,33.65 123.79,16.79 100.01,16.79Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M40.45,124.67C41.56,124.67 42.45,125.57 42.45,126.67V138.76C42.45,139.86 41.56,140.76 40.45,140.76C39.35,140.76 38.45,139.86 38.45,138.76V126.67C38.45,125.57 39.35,124.67 40.45,124.67Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M54,134.41C54.34,135.46 53.76,136.59 52.71,136.93L41.06,140.66C40.01,141 38.88,140.42 38.55,139.37C38.21,138.32 38.79,137.19 39.84,136.85L51.49,133.12C52.54,132.78 53.66,133.36 54,134.41Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M39.28,137.14C40.18,136.49 41.43,136.69 42.07,137.59L49.21,147.48C49.86,148.37 49.66,149.62 48.76,150.27C47.87,150.91 46.62,150.71 45.97,149.82L38.83,139.93C38.18,139.03 38.38,137.78 39.28,137.14Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M41.61,137.13C42.51,137.77 42.72,139.02 42.08,139.92L35.05,149.81C34.41,150.71 33.16,150.92 32.26,150.28C31.36,149.64 31.15,148.39 31.79,147.49L38.82,137.6C39.46,136.7 40.71,136.49 41.61,137.13Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M27.01,134.41C27.35,133.36 28.48,132.78 29.53,133.12L41.07,136.86C42.12,137.2 42.69,138.32 42.35,139.38C42.01,140.43 40.89,141 39.83,140.66L28.3,136.93C27.25,136.59 26.67,135.46 27.01,134.41Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M79.78,124.67C80.88,124.67 81.78,125.57 81.78,126.67V138.76C81.78,139.86 80.88,140.76 79.78,140.76C78.67,140.76 77.78,139.86 77.78,138.76V126.67C77.78,125.57 78.67,124.67 79.78,124.67Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M93.22,134.41C93.56,135.46 92.98,136.59 91.93,136.93L80.39,140.66C79.34,141 78.22,140.43 77.88,139.38C77.54,138.32 78.11,137.2 79.16,136.86L90.7,133.12C91.75,132.78 92.88,133.36 93.22,134.41Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M78.61,137.14C79.5,136.49 80.75,136.69 81.4,137.59L88.54,147.48C89.19,148.37 88.99,149.62 88.09,150.27C87.2,150.91 85.95,150.71 85.3,149.82L78.16,139.93C77.51,139.03 77.71,137.78 78.61,137.14Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M80.95,137.14C81.84,137.78 82.05,139.03 81.4,139.93L74.26,149.82C73.61,150.71 72.36,150.91 71.46,150.27C70.57,149.62 70.37,148.37 71.01,147.48L78.16,137.59C78.8,136.69 80.05,136.49 80.95,137.14Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M66.34,134.41C66.68,133.36 67.81,132.78 68.86,133.12L80.39,136.86C81.44,137.2 82.02,138.32 81.68,139.38C81.34,140.43 80.21,141 79.16,140.66L67.62,136.93C66.57,136.59 66,135.46 66.34,134.41Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M105.6,148.65C105.6,147.54 106.5,146.65 107.6,146.65H130.68C131.78,146.65 132.68,147.54 132.68,148.65C132.68,149.75 131.78,150.65 130.68,150.65H107.6C106.5,150.65 105.6,149.75 105.6,148.65Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M144.94,148.65C144.94,147.54 145.83,146.65 146.94,146.65H170.01C171.12,146.65 172.01,147.54 172.01,148.65C172.01,149.75 171.12,150.65 170.01,150.65H146.94C145.83,150.65 144.94,149.75 144.94,148.65Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M9.67,137.72C9.67,122.8 21.76,110.71 36.68,110.71H163.33C178.25,110.71 190.34,122.8 190.34,137.72C190.34,152.63 178.25,164.73 163.33,164.73H36.68C21.76,164.73 9.67,152.63 9.67,137.72ZM36.68,114.71C23.97,114.71 13.67,125.01 13.67,137.72C13.67,150.43 23.97,160.73 36.68,160.73H163.33C176.04,160.73 186.34,150.43 186.34,137.72C186.34,125.01 176.04,114.71 163.33,114.71H36.68Z"
|
||||
android:fillColor="#10949D"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -982,4 +982,9 @@ Do you want to switch to this account?</string>
|
|||
<string name="email_address_required">Email address (required)</string>
|
||||
<string name="select_the_link_in_the_email_to_verify_your_email_address_and_continue_creating_your_account">"Select the link in the email to verify your email address and continue creating your account. "</string>
|
||||
<string name="change_email_address">Change email address</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="bitwarden_cannot_recover_a_lost_or_forgotten_master_password">Bitwarden cannot recover a lost or forgotten master password.</string>
|
||||
<string name="choose_your_master_password">Choose your master password</string>
|
||||
<string name="choose_a_unique_and_strong_password_to_keep_your_information_safe">Choose a unique and strong password to keep your information safe.</string>
|
||||
<string name="minimum_characters">%1$s characters</string>
|
||||
</resources>
|
||||
|
|
|
@ -2,8 +2,12 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration
|
|||
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
|
@ -13,18 +17,16 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ContinueWithBreachedPasswordClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CreateAccountClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ErrorDialogDismiss
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordHintChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -35,16 +37,14 @@ import kotlinx.coroutines.flow.update
|
|||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
class CompleteRegistrationScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToLandingCalled = false
|
||||
|
||||
private val intentManager = mockk<IntentManager>(relaxed = true) {
|
||||
every { startCustomTabsActivity(any()) } just runs
|
||||
every { startActivity(any()) } just runs
|
||||
}
|
||||
private var onNavigateToPreventAccountLockoutCalled = false
|
||||
private var onNavigateToPasswordGuidanceCalled = false
|
||||
private var onNavigateToLoginCalled = false
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<CompleteRegistrationEvent>()
|
||||
|
@ -59,23 +59,58 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
CompleteRegistrationScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToLanding = { onNavigateToLandingCalled = true },
|
||||
intentManager = intentManager,
|
||||
onNavigateToPasswordGuidance = { onNavigateToPasswordGuidanceCalled = true },
|
||||
onNavigateToPreventAccountLockout = {
|
||||
onNavigateToPreventAccountLockoutCalled = true
|
||||
},
|
||||
onNavigateToLogin = { email, captchaToken ->
|
||||
onNavigateToLoginCalled = true
|
||||
assertTrue(email == EMAIL)
|
||||
assertTrue(captchaToken == TOKEN)
|
||||
},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `app bar submit click should send CreateAccountClick action`() {
|
||||
composeTestRule.onNodeWithText("Create account").performClick()
|
||||
verify { viewModel.trySendAction(CreateAccountClick) }
|
||||
fun `call to action with valid input click should send CreateAccountClick action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
passwordInput = "password1234",
|
||||
confirmPasswordInput = "password1234",
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasText("Create account") and hasClickAction())
|
||||
.performScrollTo()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `call to action not enabled if passwords don't match`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
passwordInput = "4321drowssap",
|
||||
confirmPasswordInput = "password1234",
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasText("Create account") and hasClickAction())
|
||||
.performScrollTo()
|
||||
.assertIsNotEnabled()
|
||||
.performClick()
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close click should send CloseClick action`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
verify { viewModel.trySendAction(CloseClick) }
|
||||
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||
verify { viewModel.trySendAction(BackClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -93,12 +128,6 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToLogin event should invoke navigate login lambda`() {
|
||||
mutableEventFlow.tryEmit(CompleteRegistrationEvent.NavigateToLanding)
|
||||
assertTrue(onNavigateToLandingCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `password input change should send PasswordInputChange action`() {
|
||||
composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT)
|
||||
|
@ -182,7 +211,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_1)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Weak").performScrollTo().assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_2)
|
||||
|
@ -211,6 +240,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
composeTestRule
|
||||
.onAllNodesWithContentDescription("Show")
|
||||
.assertCountEquals(2)[0]
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
// after clicking there should be no Show buttons:
|
||||
|
@ -222,6 +252,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
composeTestRule
|
||||
.onAllNodesWithContentDescription("Hide")
|
||||
.assertCountEquals(2)[1]
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
// then there should be two show buttons again
|
||||
|
@ -230,6 +261,80 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
.assertCountEquals(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Click on action card should send MakePasswordStrongClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn more")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Click on prevent account lockout should send LearnToPreventLockoutClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about other ways to prevent account lockout")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `NavigateToPreventAccountLockout event should invoke navigate to prevent account lockout lambda`() {
|
||||
mutableEventFlow.tryEmit(CompleteRegistrationEvent.NavigateToPreventAccountLockout)
|
||||
assertTrue(onNavigateToPreventAccountLockoutCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToPasswordGuidance event should invoke navigate to password guidance lambda`() {
|
||||
mutableEventFlow.tryEmit(CompleteRegistrationEvent.NavigateToMakePasswordStrong)
|
||||
assertTrue(onNavigateToPasswordGuidanceCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Header should be displayed in portrait mode`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Choose your master password")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Choose a unique and strong password to keep your information safe.")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Config(qualifiers = "land")
|
||||
@Test
|
||||
fun `Header should be displayed in landscape mode`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Choose your master password")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Choose a unique and strong password to keep your information safe.")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToLogin event should invoke navigate to login lambda`() {
|
||||
mutableEventFlow.tryEmit(
|
||||
CompleteRegistrationEvent.NavigateToLogin(
|
||||
email = EMAIL,
|
||||
captchaToken = TOKEN,
|
||||
),
|
||||
)
|
||||
|
||||
assertTrue(onNavigateToLoginCalled)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@test.com"
|
||||
private const val TOKEN = "token"
|
||||
|
@ -244,6 +349,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
isCheckDataBreachesToggled = true,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
onBoardingEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,14 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.Internal.ReceivePasswordStrengthResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordHintChange
|
||||
|
@ -28,12 +30,14 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
|||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -52,7 +56,9 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
private val specialCircumstanceManager: SpecialCircumstanceManager =
|
||||
SpecialCircumstanceManagerImpl()
|
||||
|
||||
private var viewmodelVerifyEmailCalled = false
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
||||
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
|
@ -86,13 +92,14 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
authRepository = mockAuthRepository,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
viewModel.onCleared()
|
||||
assertTrue(specialCircumstanceManager.specialCircumstance == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick with password below 12 chars should show password length dialog`() =
|
||||
fun `Password below 12 chars should have non-valid state`() =
|
||||
runTest {
|
||||
val input = "abcdefghikl"
|
||||
coEvery {
|
||||
|
@ -100,47 +107,24 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
} returns PasswordStrengthResult.Error
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.trySendAction(PasswordInputChange(input))
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
passwordInput = input,
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_length_val_message_x.asText(12),
|
||||
),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
|
||||
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick with passwords not matching should show password match dialog`() =
|
||||
fun `Passwords not matching should have non-valid state`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD)
|
||||
} returns PasswordStrengthResult.Error
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.trySendAction(PasswordInputChange(PASSWORD))
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
userEmail = EMAIL,
|
||||
passwordInput = PASSWORD,
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
|
||||
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick with all inputs valid should show and hide loading dialog`() = runTest {
|
||||
fun `CallToActionClick with all inputs valid should show and hide loading dialog`() = runTest {
|
||||
val repo = mockk<AuthRepository> {
|
||||
coEvery {
|
||||
register(
|
||||
|
@ -159,13 +143,16 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
val stateFlow = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventFlow = viewModel.eventFlow.testIn(backgroundScope)
|
||||
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem())
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
assertEquals(
|
||||
VALID_INPUT_STATE.copy(dialog = CompleteRegistrationDialog.Loading),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
CompleteRegistrationEvent.NavigateToLanding,
|
||||
CompleteRegistrationEvent.NavigateToLogin(
|
||||
EMAIL,
|
||||
CAPTCHA_BYPASS_TOKEN,
|
||||
),
|
||||
eventFlow.awaitItem(),
|
||||
)
|
||||
// Make sure loading dialog is hidden:
|
||||
|
@ -174,7 +161,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick register returns error should update errorDialogState`() = runTest {
|
||||
fun `CallToActionClick register returns error should update errorDialogState`() = runTest {
|
||||
val repo = mockk<AuthRepository> {
|
||||
coEvery {
|
||||
register(
|
||||
|
@ -191,7 +178,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE, repo)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(VALID_INPUT_STATE, awaitItem())
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
assertEquals(
|
||||
VALID_INPUT_STATE.copy(dialog = CompleteRegistrationDialog.Loading),
|
||||
awaitItem(),
|
||||
|
@ -211,7 +198,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick register returns Success should emit NavigateToLogin`() = runTest {
|
||||
fun `CallToActionClick register returns Success should emit NavigateToLogin`() = runTest {
|
||||
val repo = mockk<AuthRepository> {
|
||||
coEvery {
|
||||
register(
|
||||
|
@ -227,9 +214,12 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE, repo)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
assertEquals(
|
||||
CompleteRegistrationEvent.NavigateToLanding,
|
||||
CompleteRegistrationEvent.NavigateToLogin(
|
||||
EMAIL,
|
||||
CAPTCHA_BYPASS_TOKEN,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
@ -266,7 +256,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick register returns ShowDataBreaches should show HaveIBeenPwned dialog`() =
|
||||
fun `CallToActionClick register returns ShowDataBreaches should show HaveIBeenPwned dialog`() =
|
||||
runTest {
|
||||
mockAuthRepository.apply {
|
||||
coEvery {
|
||||
|
@ -287,7 +277,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createCompleteRegistrationViewModel(
|
||||
completeRegistrationState = initialState,
|
||||
)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
|
@ -304,7 +294,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `CreateAccountClick register returns DataBreachAndWeakPassword should show HaveIBeenPwned dialog`() =
|
||||
fun `CallToActionClick register returns DataBreachAndWeakPassword should show HaveIBeenPwned dialog`() =
|
||||
runTest {
|
||||
mockAuthRepository.apply {
|
||||
coEvery {
|
||||
|
@ -326,7 +316,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
|
||||
val viewModel =
|
||||
createCompleteRegistrationViewModel(completeRegistrationState = initialState)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
initialState.copy(dialog = createHaveIBeenPwned()),
|
||||
|
@ -337,7 +327,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `CreateAccountClick register returns WeakPassword should show HaveIBeenPwned dialog`() =
|
||||
fun `CallToActionClick register returns WeakPassword should show HaveIBeenPwned dialog`() =
|
||||
runTest {
|
||||
mockAuthRepository.apply {
|
||||
coEvery {
|
||||
|
@ -359,7 +349,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
val viewModel =
|
||||
createCompleteRegistrationViewModel(completeRegistrationState = initialState)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CreateAccountClick)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
|
@ -377,7 +367,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
fun `CloseClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CloseClick)
|
||||
viewModel.trySendAction(BackClick)
|
||||
assertEquals(CompleteRegistrationEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
@ -498,15 +488,39 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handling LearnToPreventLockoutClick should emit NavigateToPreventAccountLockout`() =
|
||||
runTest {
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
|
||||
assertEquals(CompleteRegistrationEvent.NavigateToPreventAccountLockout, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handling MakePasswordStrongClick should emit NavigateToMakePasswordStrong`() = runTest {
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick)
|
||||
assertEquals(CompleteRegistrationEvent.NavigateToMakePasswordStrong, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCompleteRegistrationViewModel(
|
||||
completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE,
|
||||
authRepository: AuthRepository = mockAuthRepository,
|
||||
): CompleteRegistrationViewModel =
|
||||
CompleteRegistrationViewModel(
|
||||
savedStateHandle = SavedStateHandle(mapOf("state" to completeRegistrationState)),
|
||||
savedStateHandle = SavedStateHandle(
|
||||
mapOf(
|
||||
"state" to completeRegistrationState,
|
||||
),
|
||||
),
|
||||
authRepository = authRepository,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
@ -524,6 +538,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
isCheckDataBreachesToggled = true,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
onBoardingEnabled = false,
|
||||
)
|
||||
private val VALID_INPUT_STATE = CompleteRegistrationState(
|
||||
userEmail = EMAIL,
|
||||
|
@ -535,6 +550,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
isCheckDataBreachesToggled = false,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.GOOD,
|
||||
onBoardingEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue