mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
PM-24240: Remove email verification feature flag (#5605)
This commit is contained in:
parent
867e2287dc
commit
a70f441064
@ -11,8 +11,6 @@ import com.x8bit.bitwarden.ui.auth.feature.checkemail.navigateToCheckEmail
|
|||||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.completeRegistrationDestination
|
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.completeRegistrationDestination
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
|
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.popUpToCompleteRegistration
|
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.popUpToCompleteRegistration
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestination
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDestination
|
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDestination
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.navigateToEnterpriseSignOn
|
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.navigateToEnterpriseSignOn
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.environment.environmentDestination
|
import com.x8bit.bitwarden.ui.auth.feature.environment.environmentDestination
|
||||||
@ -61,18 +59,6 @@ fun NavGraphBuilder.authGraph(
|
|||||||
navigation<AuthGraphRoute>(
|
navigation<AuthGraphRoute>(
|
||||||
startDestination = LandingRoute,
|
startDestination = LandingRoute,
|
||||||
) {
|
) {
|
||||||
createAccountDestination(
|
|
||||||
onNavigateBack = { navController.popBackStack() },
|
|
||||||
onNavigateToLogin = { emailAddress, captchaToken ->
|
|
||||||
navController.navigateToLogin(
|
|
||||||
emailAddress = emailAddress,
|
|
||||||
captchaToken = captchaToken,
|
|
||||||
navOptions = navOptions {
|
|
||||||
popUpTo(route = LandingRoute)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
startRegistrationDestination(
|
startRegistrationDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToCompleteRegistration = { emailAddress, verificationToken ->
|
onNavigateToCompleteRegistration = { emailAddress, verificationToken ->
|
||||||
@ -121,7 +107,6 @@ fun NavGraphBuilder.authGraph(
|
|||||||
)
|
)
|
||||||
setPasswordDestination()
|
setPasswordDestination()
|
||||||
landingDestination(
|
landingDestination(
|
||||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
|
||||||
onNavigateToLogin = { emailAddress ->
|
onNavigateToLogin = { emailAddress ->
|
||||||
navController.navigateToLogin(
|
navController.navigateToLogin(
|
||||||
emailAddress = emailAddress,
|
emailAddress = emailAddress,
|
||||||
@ -135,7 +120,6 @@ fun NavGraphBuilder.authGraph(
|
|||||||
onNavigateToPreAuthSettings = { navController.navigateToPreAuthSettings() },
|
onNavigateToPreAuthSettings = { navController.navigateToPreAuthSettings() },
|
||||||
)
|
)
|
||||||
welcomeDestination(
|
welcomeDestination(
|
||||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
|
||||||
onNavigateToLogin = { navController.navigateToLanding() },
|
onNavigateToLogin = { navController.navigateToLanding() },
|
||||||
onNavigateToStartRegistration = { navController.navigateToStartRegistration() },
|
onNavigateToStartRegistration = { navController.navigateToStartRegistration() },
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
|
||||||
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
|
||||||
import androidx.navigation.NavOptions
|
|
||||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type-safe route for the create account screen.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data object CreateAccountRoute
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to the create account screen.
|
|
||||||
*/
|
|
||||||
fun NavController.navigateToCreateAccount(navOptions: NavOptions? = null) {
|
|
||||||
this.navigate(route = CreateAccountRoute, navOptions = navOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the create account screen to the nav graph.
|
|
||||||
*/
|
|
||||||
fun NavGraphBuilder.createAccountDestination(
|
|
||||||
onNavigateBack: () -> Unit,
|
|
||||||
onNavigateToLogin: (emailAddress: String, captchaToken: String?) -> Unit,
|
|
||||||
) {
|
|
||||||
composableWithSlideTransitions<CreateAccountRoute> {
|
|
||||||
CreateAccountScreen(
|
|
||||||
onNavigateBack = onNavigateBack,
|
|
||||||
onNavigateToLogin = onNavigateToLogin,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,329 +0,0 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
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.Modifier
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
|
||||||
import androidx.compose.ui.semantics.customActions
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
|
||||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
|
||||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
|
||||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
|
||||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
|
||||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
|
||||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
|
||||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
|
||||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ContinueWithBreachedPasswordClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ErrorDialogDismiss
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms
|
|
||||||
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
|
|
||||||
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.composition.LocalIntentManager
|
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Top level composable for the create account screen.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
@Composable
|
|
||||||
fun CreateAccountScreen(
|
|
||||||
onNavigateBack: () -> Unit,
|
|
||||||
onNavigateToLogin: (emailAddress: String, captchaToken: String?) -> Unit,
|
|
||||||
intentManager: IntentManager = LocalIntentManager.current,
|
|
||||||
viewModel: CreateAccountViewModel = hiltViewModel(),
|
|
||||||
) {
|
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
|
||||||
EventsEffect(viewModel) { event ->
|
|
||||||
when (event) {
|
|
||||||
is NavigateToPrivacyPolicy -> {
|
|
||||||
intentManager.launchUri("https://bitwarden.com/privacy/".toUri())
|
|
||||||
}
|
|
||||||
|
|
||||||
is NavigateToTerms -> {
|
|
||||||
intentManager.launchUri("https://bitwarden.com/terms/".toUri())
|
|
||||||
}
|
|
||||||
|
|
||||||
is CreateAccountEvent.NavigateBack -> onNavigateBack.invoke()
|
|
||||||
|
|
||||||
is CreateAccountEvent.NavigateToCaptcha -> {
|
|
||||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
is CreateAccountEvent.NavigateToLogin -> {
|
|
||||||
onNavigateToLogin(
|
|
||||||
event.email,
|
|
||||||
event.captchaToken,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show dialog if needed:
|
|
||||||
when (val dialog = state.dialog) {
|
|
||||||
is CreateAccountDialog.Error -> {
|
|
||||||
BitwardenBasicDialog(
|
|
||||||
title = dialog.title?.invoke(),
|
|
||||||
message = dialog.message(),
|
|
||||||
throwable = dialog.error,
|
|
||||||
onDismissRequest = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(ErrorDialogDismiss) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is CreateAccountDialog.HaveIBeenPwned -> {
|
|
||||||
BitwardenTwoButtonDialog(
|
|
||||||
title = dialog.title(),
|
|
||||||
message = dialog.message(),
|
|
||||||
confirmButtonText = stringResource(id = BitwardenString.yes),
|
|
||||||
dismissButtonText = stringResource(id = BitwardenString.no),
|
|
||||||
onConfirmClick = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(ContinueWithBreachedPasswordClick) }
|
|
||||||
},
|
|
||||||
onDismissClick = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(ErrorDialogDismiss) }
|
|
||||||
},
|
|
||||||
onDismissRequest = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(ErrorDialogDismiss) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateAccountDialog.Loading -> {
|
|
||||||
BitwardenLoadingDialog(text = stringResource(id = BitwardenString.create_account))
|
|
||||||
}
|
|
||||||
|
|
||||||
null -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
BitwardenScaffold(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
||||||
topBar = {
|
|
||||||
BitwardenTopAppBar(
|
|
||||||
title = stringResource(id = BitwardenString.create_account),
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
|
||||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
|
||||||
onNavigationIconClick = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(CloseClick) }
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
BitwardenTextButton(
|
|
||||||
label = stringResource(id = BitwardenString.submit),
|
|
||||||
onClick = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(SubmitClick) }
|
|
||||||
},
|
|
||||||
modifier = Modifier.testTag("SubmitButton"),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
|
||||||
BitwardenTextField(
|
|
||||||
label = stringResource(id = BitwardenString.email_address),
|
|
||||||
value = state.emailInput,
|
|
||||||
onValueChange = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(EmailInputChange(it)) }
|
|
||||||
},
|
|
||||||
keyboardType = KeyboardType.Email,
|
|
||||||
textFieldTestTag = "EmailAddressEntry",
|
|
||||||
cardStyle = CardStyle.Full,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.standardHorizontalMargin(),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
|
||||||
var showPassword by rememberSaveable { mutableStateOf(false) }
|
|
||||||
BitwardenPasswordField(
|
|
||||||
label = stringResource(id = BitwardenString.master_password),
|
|
||||||
showPassword = showPassword,
|
|
||||||
showPasswordChange = { showPassword = it },
|
|
||||||
value = state.passwordInput,
|
|
||||||
onValueChange = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(PasswordInputChange(it)) }
|
|
||||||
},
|
|
||||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
|
||||||
supportingContent = {
|
|
||||||
PasswordStrengthIndicator(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
state = state.passwordStrengthState,
|
|
||||||
currentCharacterCount = state.passwordInput.length,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = state.passwordLengthLabel(),
|
|
||||||
style = BitwardenTheme.typography.bodySmall,
|
|
||||||
color = BitwardenTheme.colorScheme.text.secondary,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
passwordFieldTestTag = "MasterPasswordEntry",
|
|
||||||
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.standardHorizontalMargin(),
|
|
||||||
)
|
|
||||||
BitwardenPasswordField(
|
|
||||||
label = stringResource(id = BitwardenString.retype_master_password),
|
|
||||||
value = state.confirmPasswordInput,
|
|
||||||
showPassword = showPassword,
|
|
||||||
showPasswordChange = { showPassword = it },
|
|
||||||
onValueChange = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
|
|
||||||
},
|
|
||||||
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
|
|
||||||
passwordFieldTestTag = "ConfirmMasterPasswordEntry",
|
|
||||||
cardStyle = CardStyle.Middle(dividerPadding = 0.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.standardHorizontalMargin(),
|
|
||||||
)
|
|
||||||
BitwardenTextField(
|
|
||||||
label = stringResource(id = BitwardenString.master_password_hint),
|
|
||||||
value = state.passwordHintInput,
|
|
||||||
onValueChange = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(PasswordHintChange(it)) }
|
|
||||||
},
|
|
||||||
supportingText = stringResource(
|
|
||||||
id = BitwardenString.master_password_hint_description),
|
|
||||||
textFieldTestTag = "MasterPasswordHintLabel",
|
|
||||||
cardStyle = CardStyle.Bottom,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.standardHorizontalMargin(),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
|
||||||
BitwardenSwitch(
|
|
||||||
label = stringResource(
|
|
||||||
id = BitwardenString.check_known_data_breaches_for_this_password),
|
|
||||||
isChecked = state.isCheckDataBreachesToggled,
|
|
||||||
onCheckedChange = remember(viewModel) {
|
|
||||||
{ newState ->
|
|
||||||
viewModel.trySendAction(CheckDataBreachesToggle(newState = newState))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cardStyle = CardStyle.Top(),
|
|
||||||
modifier = Modifier
|
|
||||||
.testTag("CheckExposedMasterPasswordToggle")
|
|
||||||
.fillMaxWidth()
|
|
||||||
.standardHorizontalMargin(),
|
|
||||||
)
|
|
||||||
TermsAndPrivacySwitch(
|
|
||||||
isChecked = state.isAcceptPoliciesToggled,
|
|
||||||
onCheckedChange = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(AcceptPoliciesToggle(it)) }
|
|
||||||
},
|
|
||||||
onTermsClick = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(TermsClick) }
|
|
||||||
},
|
|
||||||
onPrivacyPolicyClick = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(PrivacyPolicyClick) }
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.standardHorizontalMargin(),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TermsAndPrivacySwitch(
|
|
||||||
isChecked: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
onTermsClick: () -> Unit,
|
|
||||||
onPrivacyPolicyClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val strTerms = stringResource(id = BitwardenString.terms_of_service)
|
|
||||||
val strPrivacy = stringResource(id = BitwardenString.privacy_policy)
|
|
||||||
BitwardenSwitch(
|
|
||||||
modifier = modifier.semantics(mergeDescendants = true) {
|
|
||||||
customActions = listOf(
|
|
||||||
CustomAccessibilityAction(
|
|
||||||
label = strTerms,
|
|
||||||
action = {
|
|
||||||
onTermsClick()
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
|
||||||
CustomAccessibilityAction(
|
|
||||||
label = strPrivacy,
|
|
||||||
action = {
|
|
||||||
onPrivacyPolicyClick()
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = annotatedStringResource(
|
|
||||||
id = BitwardenString
|
|
||||||
.by_activating_this_switch_you_agree_to_the_terms_of_service_and_privacy_policy,
|
|
||||||
onAnnotationClick = {
|
|
||||||
when (it) {
|
|
||||||
"termsOfService" -> onTermsClick()
|
|
||||||
"privacyPolicy" -> onPrivacyPolicyClick()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
isChecked = isChecked,
|
|
||||||
contentDescription = "AcceptPoliciesToggle",
|
|
||||||
onCheckedChange = onCheckedChange,
|
|
||||||
cardStyle = CardStyle.Bottom,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,595 +0,0 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
|
||||||
import com.bitwarden.ui.platform.base.util.isValidEmail
|
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
|
||||||
import com.bitwarden.ui.util.Text
|
|
||||||
import com.bitwarden.ui.util.asText
|
|
||||||
import com.bitwarden.ui.util.concat
|
|
||||||
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.auth.repository.util.CaptchaCallbackTokenResult
|
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ContinueWithBreachedPasswordClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Internal.ReceivePasswordStrengthResult
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private const val KEY_STATE = "state"
|
|
||||||
private const val MIN_PASSWORD_LENGTH = 12
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Models logic for the create account screen.
|
|
||||||
*/
|
|
||||||
@Suppress("TooManyFunctions")
|
|
||||||
@HiltViewModel
|
|
||||||
class CreateAccountViewModel @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val authRepository: AuthRepository,
|
|
||||||
) : BaseViewModel<CreateAccountState, CreateAccountEvent, CreateAccountAction>(
|
|
||||||
initialState = savedStateHandle[KEY_STATE]
|
|
||||||
?: CreateAccountState(
|
|
||||||
emailInput = "",
|
|
||||||
passwordInput = "",
|
|
||||||
confirmPasswordInput = "",
|
|
||||||
passwordHintInput = "",
|
|
||||||
isAcceptPoliciesToggled = false,
|
|
||||||
isCheckDataBreachesToggled = true,
|
|
||||||
dialog = null,
|
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps track of async request to get password strength. Should be cancelled
|
|
||||||
* when user input changes.
|
|
||||||
*/
|
|
||||||
private var passwordStrengthJob: Job = Job().apply { complete() }
|
|
||||||
|
|
||||||
init {
|
|
||||||
// As state updates, write to saved state handle:
|
|
||||||
stateFlow
|
|
||||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
|
||||||
.launchIn(viewModelScope)
|
|
||||||
authRepository
|
|
||||||
.captchaTokenResultFlow
|
|
||||||
.onEach {
|
|
||||||
sendAction(
|
|
||||||
CreateAccountAction.Internal.ReceiveCaptchaToken(
|
|
||||||
tokenResult = it,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleAction(action: CreateAccountAction) {
|
|
||||||
when (action) {
|
|
||||||
is SubmitClick -> handleSubmitClick()
|
|
||||||
is ConfirmPasswordInputChange -> handleConfirmPasswordInputChanged(action)
|
|
||||||
is EmailInputChange -> handleEmailInputChanged(action)
|
|
||||||
is PasswordHintChange -> handlePasswordHintChanged(action)
|
|
||||||
is PasswordInputChange -> handlePasswordInputChanged(action)
|
|
||||||
is CreateAccountAction.CloseClick -> handleCloseClick()
|
|
||||||
is CreateAccountAction.ErrorDialogDismiss -> handleDialogDismiss()
|
|
||||||
is AcceptPoliciesToggle -> handleAcceptPoliciesToggle(action)
|
|
||||||
is CheckDataBreachesToggle -> handleCheckDataBreachesToggle(action)
|
|
||||||
is PrivacyPolicyClick -> handlePrivacyPolicyClick()
|
|
||||||
is TermsClick -> handleTermsClick()
|
|
||||||
is CreateAccountAction.Internal.ReceiveRegisterResult -> {
|
|
||||||
handleReceiveRegisterAccountResult(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
is CreateAccountAction.Internal.ReceiveCaptchaToken -> {
|
|
||||||
handleReceiveCaptchaToken(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
ContinueWithBreachedPasswordClick -> handleContinueWithBreachedPasswordClick()
|
|
||||||
is ReceivePasswordStrengthResult -> handlePasswordStrengthResult(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePasswordStrengthResult(action: ReceivePasswordStrengthResult) {
|
|
||||||
when (val result = action.result) {
|
|
||||||
is PasswordStrengthResult.Success -> {
|
|
||||||
val updatedState = when (result.passwordStrength) {
|
|
||||||
PasswordStrength.LEVEL_0 -> PasswordStrengthState.WEAK_1
|
|
||||||
PasswordStrength.LEVEL_1 -> PasswordStrengthState.WEAK_2
|
|
||||||
PasswordStrength.LEVEL_2 -> PasswordStrengthState.WEAK_3
|
|
||||||
PasswordStrength.LEVEL_3 -> PasswordStrengthState.GOOD
|
|
||||||
PasswordStrength.LEVEL_4 -> PasswordStrengthState.STRONG
|
|
||||||
}
|
|
||||||
mutableStateFlow.update { oldState ->
|
|
||||||
oldState.copy(
|
|
||||||
passwordStrengthState = updatedState,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is PasswordStrengthResult.Error -> {
|
|
||||||
// Leave UI the same
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleReceiveCaptchaToken(
|
|
||||||
action: CreateAccountAction.Internal.ReceiveCaptchaToken,
|
|
||||||
) {
|
|
||||||
when (val result = action.tokenResult) {
|
|
||||||
is CaptchaCallbackTokenResult.MissingToken -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.captcha_failed.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is CaptchaCallbackTokenResult.Success -> {
|
|
||||||
submitRegisterAccountRequest(
|
|
||||||
shouldCheckForDataBreaches = false,
|
|
||||||
shouldIgnorePasswordStrength = true,
|
|
||||||
captchaToken = result.token,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod", "MaxLineLength")
|
|
||||||
private fun handleReceiveRegisterAccountResult(
|
|
||||||
action: CreateAccountAction.Internal.ReceiveRegisterResult,
|
|
||||||
) {
|
|
||||||
when (val registerAccountResult = action.registerResult) {
|
|
||||||
is RegisterResult.CaptchaRequired -> {
|
|
||||||
mutableStateFlow.update { it.copy(dialog = null) }
|
|
||||||
sendEvent(
|
|
||||||
CreateAccountEvent.NavigateToCaptcha(
|
|
||||||
uri = generateUriForCaptcha(captchaId = registerAccountResult.captchaId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is RegisterResult.Error -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = registerAccountResult.errorMessage?.asText()
|
|
||||||
?: BitwardenString.generic_error_message.asText(),
|
|
||||||
error = registerAccountResult.error,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is RegisterResult.Success -> {
|
|
||||||
mutableStateFlow.update { it.copy(dialog = null) }
|
|
||||||
sendEvent(
|
|
||||||
CreateAccountEvent.NavigateToLogin(
|
|
||||||
email = state.emailInput,
|
|
||||||
captchaToken = registerAccountResult.captchaToken,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterResult.DataBreachFound -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
|
||||||
title = BitwardenString.exposed_master_password.asText(),
|
|
||||||
message = BitwardenString.password_found_in_a_data_breach_alert_description.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterResult.DataBreachAndWeakPassword -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
|
||||||
title = BitwardenString.weak_and_exposed_master_password.asText(),
|
|
||||||
message = BitwardenString.weak_password_identified_and_found_in_a_data_breach_alert_description.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterResult.WeakPassword -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
|
||||||
title = BitwardenString.weak_master_password.asText(),
|
|
||||||
message = BitwardenString.weak_password_identified_use_a_strong_password_to_protect_your_account.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePrivacyPolicyClick() = sendEvent(CreateAccountEvent.NavigateToPrivacyPolicy)
|
|
||||||
|
|
||||||
private fun handleTermsClick() = sendEvent(CreateAccountEvent.NavigateToTerms)
|
|
||||||
|
|
||||||
private fun handleAcceptPoliciesToggle(action: AcceptPoliciesToggle) {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(isAcceptPoliciesToggled = action.newState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleCheckDataBreachesToggle(action: CheckDataBreachesToggle) {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(isCheckDataBreachesToggled = action.newState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDialogDismiss() {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(dialog = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleCloseClick() {
|
|
||||||
sendEvent(CreateAccountEvent.NavigateBack)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleEmailInputChanged(action: EmailInputChange) {
|
|
||||||
mutableStateFlow.update { it.copy(emailInput = action.input) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePasswordHintChanged(action: PasswordHintChange) {
|
|
||||||
mutableStateFlow.update { it.copy(passwordHintInput = action.input) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePasswordInputChanged(action: PasswordInputChange) {
|
|
||||||
// Update input:
|
|
||||||
mutableStateFlow.update { it.copy(passwordInput = action.input) }
|
|
||||||
// Update password strength:
|
|
||||||
passwordStrengthJob.cancel()
|
|
||||||
if (action.input.isEmpty()) {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
passwordStrengthJob = viewModelScope.launch {
|
|
||||||
val result = authRepository.getPasswordStrength(
|
|
||||||
email = state.emailInput,
|
|
||||||
password = action.input,
|
|
||||||
)
|
|
||||||
trySendAction(ReceivePasswordStrengthResult(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleConfirmPasswordInputChanged(action: ConfirmPasswordInputChange) {
|
|
||||||
mutableStateFlow.update { it.copy(confirmPasswordInput = action.input) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
private fun handleSubmitClick() = when {
|
|
||||||
state.emailInput.isBlank() -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.validation_field_required
|
|
||||||
.asText(BitwardenString.email_address.asText()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!state.emailInput.isValidEmail() -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.invalid_email.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.passwordInput.length < MIN_PASSWORD_LENGTH -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.master_password_length_val_message_x
|
|
||||||
.asText(MIN_PASSWORD_LENGTH),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.passwordInput != state.confirmPasswordInput -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.master_password_confirmation_val_message.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!state.isAcceptPoliciesToggled -> {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.accept_policies_error.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
submitRegisterAccountRequest(
|
|
||||||
shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
|
|
||||||
shouldIgnorePasswordStrength = false,
|
|
||||||
captchaToken = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleContinueWithBreachedPasswordClick() {
|
|
||||||
submitRegisterAccountRequest(
|
|
||||||
shouldCheckForDataBreaches = false,
|
|
||||||
shouldIgnorePasswordStrength = true,
|
|
||||||
captchaToken = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun submitRegisterAccountRequest(
|
|
||||||
shouldCheckForDataBreaches: Boolean,
|
|
||||||
shouldIgnorePasswordStrength: Boolean,
|
|
||||||
captchaToken: String?,
|
|
||||||
) {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(dialog = CreateAccountDialog.Loading)
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
val result = authRepository.register(
|
|
||||||
shouldCheckDataBreaches = shouldCheckForDataBreaches,
|
|
||||||
isMasterPasswordStrong = shouldIgnorePasswordStrength ||
|
|
||||||
state.isMasterPasswordStrong,
|
|
||||||
email = state.emailInput,
|
|
||||||
masterPassword = state.passwordInput,
|
|
||||||
masterPasswordHint = state.passwordHintInput.ifBlank { null },
|
|
||||||
captchaToken = captchaToken,
|
|
||||||
)
|
|
||||||
sendAction(
|
|
||||||
CreateAccountAction.Internal.ReceiveRegisterResult(
|
|
||||||
registerResult = result,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI state for the create account screen.
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
data class CreateAccountState(
|
|
||||||
val emailInput: String,
|
|
||||||
val passwordInput: String,
|
|
||||||
val confirmPasswordInput: String,
|
|
||||||
val passwordHintInput: String,
|
|
||||||
val isCheckDataBreachesToggled: Boolean,
|
|
||||||
val isAcceptPoliciesToggled: Boolean,
|
|
||||||
val dialog: CreateAccountDialog?,
|
|
||||||
val passwordStrengthState: PasswordStrengthState,
|
|
||||||
) : 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() = BitwardenString.important.asText()
|
|
||||||
.concat(
|
|
||||||
": ".asText(),
|
|
||||||
BitwardenString.your_master_password_cannot_be_recovered_if_you_forget_it_x_characters_minimum
|
|
||||||
.asText(MIN_PASSWORD_LENGTH),
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the provided master password is considered strong.
|
|
||||||
*/
|
|
||||||
val isMasterPasswordStrong: Boolean
|
|
||||||
get() = when (passwordStrengthState) {
|
|
||||||
PasswordStrengthState.NONE,
|
|
||||||
PasswordStrengthState.WEAK_1,
|
|
||||||
PasswordStrengthState.WEAK_2,
|
|
||||||
PasswordStrengthState.WEAK_3,
|
|
||||||
-> false
|
|
||||||
|
|
||||||
PasswordStrengthState.GOOD,
|
|
||||||
PasswordStrengthState.STRONG,
|
|
||||||
-> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Models dialogs that can be displayed on the create account screen.
|
|
||||||
*/
|
|
||||||
sealed class CreateAccountDialog : Parcelable {
|
|
||||||
/**
|
|
||||||
* Loading dialog.
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
data object Loading : CreateAccountDialog()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm the user wants to continue with potentially breached password.
|
|
||||||
*
|
|
||||||
* @param title The title for the HaveIBeenPwned dialog.
|
|
||||||
* @param message The message for the HaveIBeenPwned dialog.
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
data class HaveIBeenPwned(
|
|
||||||
val title: Text,
|
|
||||||
val message: Text,
|
|
||||||
) : CreateAccountDialog()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General error dialog with an OK button.
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
data class Error(
|
|
||||||
val title: Text?,
|
|
||||||
val message: Text,
|
|
||||||
val error: Throwable? = null,
|
|
||||||
) : CreateAccountDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Models events for the create account screen.
|
|
||||||
*/
|
|
||||||
sealed class CreateAccountEvent {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate back to previous screen.
|
|
||||||
*/
|
|
||||||
data object NavigateBack : CreateAccountEvent()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the captcha verification screen.
|
|
||||||
*/
|
|
||||||
data class NavigateToCaptcha(val uri: Uri) : CreateAccountEvent()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the login screen bypassing captcha with token.
|
|
||||||
*/
|
|
||||||
data class NavigateToLogin(
|
|
||||||
val email: String,
|
|
||||||
val captchaToken: String?,
|
|
||||||
) : CreateAccountEvent()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to terms and conditions.
|
|
||||||
*/
|
|
||||||
data object NavigateToTerms : CreateAccountEvent()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to privacy policy.
|
|
||||||
*/
|
|
||||||
data object NavigateToPrivacyPolicy : CreateAccountEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Models actions for the create account screen.
|
|
||||||
*/
|
|
||||||
sealed class CreateAccountAction {
|
|
||||||
/**
|
|
||||||
* User clicked submit.
|
|
||||||
*/
|
|
||||||
data object SubmitClick : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User clicked close.
|
|
||||||
*/
|
|
||||||
data object CloseClick : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User clicked "Yes" when being asked if they are sure they want to use a breached password.
|
|
||||||
*/
|
|
||||||
data object ContinueWithBreachedPasswordClick : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Email input changed.
|
|
||||||
*/
|
|
||||||
data class EmailInputChange(val input: String) : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Password input changed.
|
|
||||||
*/
|
|
||||||
data class PasswordInputChange(val input: String) : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm password input changed.
|
|
||||||
*/
|
|
||||||
data class ConfirmPasswordInputChange(val input: String) : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Password hint input changed.
|
|
||||||
*/
|
|
||||||
data class PasswordHintChange(val input: String) : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User dismissed the error dialog.
|
|
||||||
*/
|
|
||||||
data object ErrorDialogDismiss : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User tapped check data breaches toggle.
|
|
||||||
*/
|
|
||||||
data class CheckDataBreachesToggle(val newState: Boolean) : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User tapped accept policies toggle.
|
|
||||||
*/
|
|
||||||
data class AcceptPoliciesToggle(val newState: Boolean) : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User tapped privacy policy link.
|
|
||||||
*/
|
|
||||||
data object PrivacyPolicyClick : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User tapped terms link.
|
|
||||||
*/
|
|
||||||
data object TermsClick : CreateAccountAction()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Models actions that the [CreateAccountViewModel] itself might send.
|
|
||||||
*/
|
|
||||||
sealed class Internal : CreateAccountAction() {
|
|
||||||
/**
|
|
||||||
* Indicates a captcha callback token has been received.
|
|
||||||
*/
|
|
||||||
data class ReceiveCaptchaToken(
|
|
||||||
val tokenResult: CaptchaCallbackTokenResult,
|
|
||||||
) : Internal()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates a [RegisterResult] has been received.
|
|
||||||
*/
|
|
||||||
data class ReceiveRegisterResult(
|
|
||||||
val registerResult: RegisterResult,
|
|
||||||
) : Internal()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates a password strength result has been received.
|
|
||||||
*/
|
|
||||||
data class ReceivePasswordStrengthResult(
|
|
||||||
val result: PasswordStrengthResult,
|
|
||||||
) : Internal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -23,7 +23,6 @@ fun NavController.navigateToLanding(navOptions: NavOptions? = null) {
|
|||||||
* Add the Landing screen to the nav graph.
|
* Add the Landing screen to the nav graph.
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.landingDestination(
|
fun NavGraphBuilder.landingDestination(
|
||||||
onNavigateToCreateAccount: () -> Unit,
|
|
||||||
onNavigateToLogin: (emailAddress: String) -> Unit,
|
onNavigateToLogin: (emailAddress: String) -> Unit,
|
||||||
onNavigateToEnvironment: () -> Unit,
|
onNavigateToEnvironment: () -> Unit,
|
||||||
onNavigateToStartRegistration: () -> Unit,
|
onNavigateToStartRegistration: () -> Unit,
|
||||||
@ -31,7 +30,6 @@ fun NavGraphBuilder.landingDestination(
|
|||||||
) {
|
) {
|
||||||
composableWithStayTransitions<LandingRoute> {
|
composableWithStayTransitions<LandingRoute> {
|
||||||
LandingScreen(
|
LandingScreen(
|
||||||
onNavigateToCreateAccount = onNavigateToCreateAccount,
|
|
||||||
onNavigateToLogin = onNavigateToLogin,
|
onNavigateToLogin = onNavigateToLogin,
|
||||||
onNavigateToEnvironment = onNavigateToEnvironment,
|
onNavigateToEnvironment = onNavigateToEnvironment,
|
||||||
onNavigateToStartRegistration = onNavigateToStartRegistration,
|
onNavigateToStartRegistration = onNavigateToStartRegistration,
|
||||||
|
|||||||
@ -65,7 +65,6 @@ import kotlinx.collections.immutable.toImmutableList
|
|||||||
@Composable
|
@Composable
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
fun LandingScreen(
|
fun LandingScreen(
|
||||||
onNavigateToCreateAccount: () -> Unit,
|
|
||||||
onNavigateToLogin: (emailAddress: String) -> Unit,
|
onNavigateToLogin: (emailAddress: String) -> Unit,
|
||||||
onNavigateToEnvironment: () -> Unit,
|
onNavigateToEnvironment: () -> Unit,
|
||||||
onNavigateToStartRegistration: () -> Unit,
|
onNavigateToStartRegistration: () -> Unit,
|
||||||
@ -76,7 +75,6 @@ fun LandingScreen(
|
|||||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
LandingEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
|
|
||||||
is LandingEvent.NavigateToLogin -> onNavigateToLogin(event.emailAddress)
|
is LandingEvent.NavigateToLogin -> onNavigateToLogin(event.emailAddress)
|
||||||
LandingEvent.NavigateToEnvironment -> onNavigateToEnvironment()
|
LandingEvent.NavigateToEnvironment -> onNavigateToEnvironment()
|
||||||
LandingEvent.NavigateToStartRegistration -> onNavigateToStartRegistration()
|
LandingEvent.NavigateToStartRegistration -> onNavigateToStartRegistration()
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
|
||||||
import com.bitwarden.data.repository.model.Environment
|
import com.bitwarden.data.repository.model.Environment
|
||||||
import com.bitwarden.ui.platform.base.BackgroundEvent
|
import com.bitwarden.ui.platform.base.BackgroundEvent
|
||||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||||
@ -14,7 +13,6 @@ import com.bitwarden.ui.util.asText
|
|||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
|
||||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
@ -41,7 +39,6 @@ class LandingViewModel @Inject constructor(
|
|||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val vaultRepository: VaultRepository,
|
private val vaultRepository: VaultRepository,
|
||||||
private val environmentRepository: EnvironmentRepository,
|
private val environmentRepository: EnvironmentRepository,
|
||||||
private val featureFlagManager: FeatureFlagManager,
|
|
||||||
snackbarRelayManager: SnackbarRelayManager,
|
snackbarRelayManager: SnackbarRelayManager,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
) : BaseViewModel<LandingState, LandingEvent, LandingAction>(
|
) : BaseViewModel<LandingState, LandingEvent, LandingAction>(
|
||||||
@ -215,13 +212,7 @@ class LandingViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCreateAccountClicked() {
|
private fun handleCreateAccountClicked() {
|
||||||
val navigationEvent =
|
sendEvent(LandingEvent.NavigateToStartRegistration)
|
||||||
if (featureFlagManager.getFeatureFlag(key = FlagKey.EmailVerification)) {
|
|
||||||
LandingEvent.NavigateToStartRegistration
|
|
||||||
} else {
|
|
||||||
LandingEvent.NavigateToCreateAccount
|
|
||||||
}
|
|
||||||
sendEvent(navigationEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDialogDismiss() {
|
private fun handleDialogDismiss() {
|
||||||
@ -326,11 +317,6 @@ data class LandingState(
|
|||||||
* Models events for the landing screen.
|
* Models events for the landing screen.
|
||||||
*/
|
*/
|
||||||
sealed class LandingEvent {
|
sealed class LandingEvent {
|
||||||
/**
|
|
||||||
* Navigates to the Create Account screen.
|
|
||||||
*/
|
|
||||||
data object NavigateToCreateAccount : LandingEvent()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to the Start Registration screen.
|
* Navigates to the Start Registration screen.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -23,13 +23,11 @@ fun NavController.navigateToWelcome(navOptions: NavOptions? = null) {
|
|||||||
* Add the Welcome screen to the nav graph.
|
* Add the Welcome screen to the nav graph.
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.welcomeDestination(
|
fun NavGraphBuilder.welcomeDestination(
|
||||||
onNavigateToCreateAccount: () -> Unit,
|
|
||||||
onNavigateToLogin: () -> Unit,
|
onNavigateToLogin: () -> Unit,
|
||||||
onNavigateToStartRegistration: () -> Unit,
|
onNavigateToStartRegistration: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithStayTransitions<WelcomeRoute> {
|
composableWithStayTransitions<WelcomeRoute> {
|
||||||
WelcomeScreen(
|
WelcomeScreen(
|
||||||
onNavigateToCreateAccount = onNavigateToCreateAccount,
|
|
||||||
onNavigateToLogin = onNavigateToLogin,
|
onNavigateToLogin = onNavigateToLogin,
|
||||||
onNavigateToStartRegistration = onNavigateToStartRegistration,
|
onNavigateToStartRegistration = onNavigateToStartRegistration,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -63,7 +63,6 @@ private val HORIZONTAL_MARGIN_MEDIUM: Dp = 128.dp
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun WelcomeScreen(
|
fun WelcomeScreen(
|
||||||
onNavigateToCreateAccount: () -> Unit,
|
|
||||||
onNavigateToLogin: () -> Unit,
|
onNavigateToLogin: () -> Unit,
|
||||||
onNavigateToStartRegistration: () -> Unit,
|
onNavigateToStartRegistration: () -> Unit,
|
||||||
viewModel: WelcomeViewModel = hiltViewModel(),
|
viewModel: WelcomeViewModel = hiltViewModel(),
|
||||||
@ -78,7 +77,6 @@ fun WelcomeScreen(
|
|||||||
scope.launch { pagerState.animateScrollToPage(event.index) }
|
scope.launch { pagerState.animateScrollToPage(event.index) }
|
||||||
}
|
}
|
||||||
|
|
||||||
WelcomeEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
|
|
||||||
WelcomeEvent.NavigateToLogin -> onNavigateToLogin()
|
WelcomeEvent.NavigateToLogin -> onNavigateToLogin()
|
||||||
WelcomeEvent.NavigateToStartRegistration -> onNavigateToStartRegistration()
|
WelcomeEvent.NavigateToStartRegistration -> onNavigateToStartRegistration()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.welcome
|
package com.x8bit.bitwarden.ui.auth.feature.welcome
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
|
||||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@ -15,9 +13,7 @@ import javax.inject.Inject
|
|||||||
* Manages application state for the welcome screen.
|
* Manages application state for the welcome screen.
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class WelcomeViewModel @Inject constructor(
|
class WelcomeViewModel @Inject constructor() :
|
||||||
private val featureFlagManager: FeatureFlagManager,
|
|
||||||
) :
|
|
||||||
BaseViewModel<WelcomeState, WelcomeEvent, WelcomeAction>(
|
BaseViewModel<WelcomeState, WelcomeEvent, WelcomeAction>(
|
||||||
initialState = WelcomeState(
|
initialState = WelcomeState(
|
||||||
index = 0,
|
index = 0,
|
||||||
@ -48,12 +44,7 @@ class WelcomeViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCreateAccountClick() {
|
private fun handleCreateAccountClick() {
|
||||||
val event = if (featureFlagManager.getFeatureFlag(FlagKey.EmailVerification)) {
|
sendEvent(WelcomeEvent.NavigateToStartRegistration)
|
||||||
WelcomeEvent.NavigateToStartRegistration
|
|
||||||
} else {
|
|
||||||
WelcomeEvent.NavigateToCreateAccount
|
|
||||||
}
|
|
||||||
sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginClick() {
|
private fun handleLoginClick() {
|
||||||
@ -130,11 +121,6 @@ sealed class WelcomeEvent {
|
|||||||
val index: Int,
|
val index: Int,
|
||||||
) : WelcomeEvent()
|
) : WelcomeEvent()
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the create account screen.
|
|
||||||
*/
|
|
||||||
data object NavigateToCreateAccount : WelcomeEvent()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to the login screen.
|
* Navigates to the login screen.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -263,7 +263,7 @@ private fun FeatureFlagContent_preview() {
|
|||||||
BitwardenTheme {
|
BitwardenTheme {
|
||||||
FeatureFlagContent(
|
FeatureFlagContent(
|
||||||
featureFlagMap = persistentMapOf(
|
featureFlagMap = persistentMapOf(
|
||||||
FlagKey.EmailVerification to true,
|
FlagKey.DummyBoolean to true,
|
||||||
),
|
),
|
||||||
onValueChange = { _, _ -> },
|
onValueChange = { _, _ -> },
|
||||||
onResetValues = { },
|
onResetValues = { },
|
||||||
|
|||||||
@ -26,7 +26,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
FlagKey.BitwardenAuthenticationEnabled,
|
FlagKey.BitwardenAuthenticationEnabled,
|
||||||
FlagKey.EmailVerification,
|
|
||||||
FlagKey.CredentialExchangeProtocolImport,
|
FlagKey.CredentialExchangeProtocolImport,
|
||||||
FlagKey.CredentialExchangeProtocolExport,
|
FlagKey.CredentialExchangeProtocolExport,
|
||||||
FlagKey.CipherKeyEncryption,
|
FlagKey.CipherKeyEncryption,
|
||||||
@ -74,7 +73,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
|||||||
FlagKey.DummyString,
|
FlagKey.DummyString,
|
||||||
-> this.keyName
|
-> this.keyName
|
||||||
|
|
||||||
FlagKey.EmailVerification -> stringResource(BitwardenString.email_verification)
|
|
||||||
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
||||||
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
||||||
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package com.x8bit.bitwarden.data.platform.manager
|
package com.x8bit.bitwarden.data.platform.manager
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.bitwarden.core.data.manager.model.FlagKey
|
||||||
import com.bitwarden.data.datasource.disk.model.ServerConfig
|
import com.bitwarden.data.datasource.disk.model.ServerConfig
|
||||||
import com.bitwarden.network.model.ConfigResponseJson
|
import com.bitwarden.network.model.ConfigResponseJson
|
||||||
import com.bitwarden.network.model.ConfigResponseJson.EnvironmentJson
|
import com.bitwarden.network.model.ConfigResponseJson.EnvironmentJson
|
||||||
import com.bitwarden.network.model.ConfigResponseJson.ServerJson
|
import com.bitwarden.network.model.ConfigResponseJson.ServerJson
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeServerConfigRepository
|
import com.x8bit.bitwarden.data.platform.repository.util.FakeServerConfigRepository
|
||||||
import com.x8bit.bitwarden.data.platform.util.isServerVersionAtLeast
|
import com.x8bit.bitwarden.data.platform.util.isServerVersionAtLeast
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@ -130,7 +130,7 @@ class FeatureFlagManagerTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val flagValue = manager.getFeatureFlag(
|
val flagValue = manager.getFeatureFlag(
|
||||||
key = FlagKey.EmailVerification,
|
key = FlagKey.DummyBoolean,
|
||||||
forceRefresh = false,
|
forceRefresh = false,
|
||||||
)
|
)
|
||||||
assertFalse(flagValue)
|
assertFalse(flagValue)
|
||||||
@ -226,7 +226,7 @@ class FeatureFlagManagerTest {
|
|||||||
fakeServerConfigRepository.serverConfigValue = null
|
fakeServerConfigRepository.serverConfigValue = null
|
||||||
|
|
||||||
val flagValue = manager.getFeatureFlag(
|
val flagValue = manager.getFeatureFlag(
|
||||||
key = FlagKey.EmailVerification,
|
key = FlagKey.DummyBoolean,
|
||||||
forceRefresh = false,
|
forceRefresh = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.x8bit.bitwarden.data.platform.repository
|
package com.x8bit.bitwarden.data.platform.repository
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.bitwarden.core.data.manager.model.FlagKey
|
||||||
import com.bitwarden.data.datasource.disk.model.ServerConfig
|
import com.bitwarden.data.datasource.disk.model.ServerConfig
|
||||||
import com.bitwarden.data.repository.ServerConfigRepository
|
import com.bitwarden.data.repository.ServerConfigRepository
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
@ -8,7 +9,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
|||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
@ -99,8 +99,8 @@ class DebugMenuRepositoryTest {
|
|||||||
debugMenuRepository.resetFeatureFlagOverrides()
|
debugMenuRepository.resetFeatureFlagOverrides()
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
||||||
FlagKey.EmailVerification,
|
FlagKey.CredentialExchangeProtocolImport,
|
||||||
FlagKey.EmailVerification.defaultValue,
|
FlagKey.CredentialExchangeProtocolImport.defaultValue,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
debugMenuRepository.featureFlagOverridesUpdatedFlow.test {
|
debugMenuRepository.featureFlagOverridesUpdatedFlow.test {
|
||||||
|
|||||||
@ -1,319 +0,0 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.ui.test.assertCountEquals
|
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
|
||||||
import androidx.compose.ui.test.assertIsOff
|
|
||||||
import androidx.compose.ui.test.assertIsOn
|
|
||||||
import androidx.compose.ui.test.filterToOne
|
|
||||||
import androidx.compose.ui.test.hasAnyAncestor
|
|
||||||
import androidx.compose.ui.test.isDialog
|
|
||||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.compose.ui.test.performClick
|
|
||||||
import androidx.compose.ui.test.performScrollTo
|
|
||||||
import androidx.compose.ui.test.performTextInput
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
|
||||||
import com.bitwarden.ui.util.asText
|
|
||||||
import com.bitwarden.ui.util.performCustomAccessibilityAction
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.runs
|
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class CreateAccountScreenTest : BitwardenComposeTest() {
|
|
||||||
|
|
||||||
private var onNavigateBackCalled = false
|
|
||||||
private var onNavigateToLoginCalled = false
|
|
||||||
|
|
||||||
private val intentManager = mockk<IntentManager>(relaxed = true) {
|
|
||||||
every { startCustomTabsActivity(any()) } just runs
|
|
||||||
every { startActivity(any()) } just runs
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<CreateAccountEvent>()
|
|
||||||
private val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
|
||||||
every { stateFlow } returns mutableStateFlow
|
|
||||||
every { eventFlow } returns mutableEventFlow
|
|
||||||
every { trySendAction(any()) } just runs
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
setContent(
|
|
||||||
intentManager = intentManager,
|
|
||||||
) {
|
|
||||||
CreateAccountScreen(
|
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
|
||||||
onNavigateToLogin = { _, _ -> onNavigateToLoginCalled = true },
|
|
||||||
viewModel = viewModel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `app bar submit click should send SubmitClick action`() {
|
|
||||||
composeTestRule.onNodeWithText("Submit").performClick()
|
|
||||||
verify { viewModel.trySendAction(SubmitClick) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `close click should send CloseClick action`() {
|
|
||||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
|
||||||
verify { viewModel.trySendAction(CloseClick) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `check data breaches click should send CheckDataBreachesToggle action`() {
|
|
||||||
composeTestRule
|
|
||||||
.onNodeWithText("Check known data breaches for this password")
|
|
||||||
.performScrollTo()
|
|
||||||
.performClick()
|
|
||||||
verify { viewModel.trySendAction(CheckDataBreachesToggle(true)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `accept policies should be toggled on or off according to the state`() {
|
|
||||||
composeTestRule
|
|
||||||
.onNodeWithText("By activating this switch, you agree", substring = true)
|
|
||||||
.assertIsOff()
|
|
||||||
|
|
||||||
mutableStateFlow.update { it.copy(isAcceptPoliciesToggled = true) }
|
|
||||||
|
|
||||||
composeTestRule
|
|
||||||
.onNodeWithText("By activating this switch, you agree", substring = true)
|
|
||||||
.assertIsOn()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `accept policies click should send AcceptPoliciesToggle action`() {
|
|
||||||
composeTestRule
|
|
||||||
.onNodeWithText("By activating this switch, you agree", substring = true)
|
|
||||||
.performScrollTo()
|
|
||||||
.performClick()
|
|
||||||
verify { viewModel.trySendAction(AcceptPoliciesToggle(true)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateBack event should invoke navigate back lambda`() {
|
|
||||||
mutableEventFlow.tryEmit(CreateAccountEvent.NavigateBack)
|
|
||||||
assertTrue(onNavigateBackCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateToLogin event should invoke navigate login lambda`() {
|
|
||||||
mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToLogin(email = "", captchaToken = ""))
|
|
||||||
assertTrue(onNavigateToLoginCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateToCaptcha event should invoke intent manager`() {
|
|
||||||
val mockUri = mockk<Uri>()
|
|
||||||
mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToCaptcha(uri = mockUri))
|
|
||||||
verify {
|
|
||||||
intentManager.startCustomTabsActivity(mockUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateToPrivacyPolicy event should invoke intent manager`() {
|
|
||||||
mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToPrivacyPolicy)
|
|
||||||
verify {
|
|
||||||
intentManager.launchUri("https://bitwarden.com/privacy/".toUri())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateToTerms event should invoke intent manager`() {
|
|
||||||
mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToTerms)
|
|
||||||
verify {
|
|
||||||
intentManager.launchUri("https://bitwarden.com/terms/".toUri())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `email input change should send EmailInputChange action`() {
|
|
||||||
composeTestRule.onNodeWithText("Email address").performTextInput(TEST_INPUT)
|
|
||||||
verify { viewModel.trySendAction(EmailInputChange(TEST_INPUT)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `password input change should send PasswordInputChange action`() {
|
|
||||||
composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT)
|
|
||||||
verify { viewModel.trySendAction(PasswordInputChange(TEST_INPUT)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `confirm password input change should send ConfirmPasswordInputChange action`() {
|
|
||||||
composeTestRule.onNodeWithText("Re-type master password").performTextInput(TEST_INPUT)
|
|
||||||
verify { viewModel.trySendAction(ConfirmPasswordInputChange(TEST_INPUT)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `password hint input change should send PasswordHintChange action`() {
|
|
||||||
composeTestRule
|
|
||||||
.onNodeWithText("Master password hint (optional)")
|
|
||||||
.performTextInput(TEST_INPUT)
|
|
||||||
verify { viewModel.trySendAction(PasswordHintChange(TEST_INPUT)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking OK on the error dialog should send ErrorDialogDismiss action`() {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = "title".asText(),
|
|
||||||
message = "message".asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composeTestRule
|
|
||||||
.onAllNodesWithText(text = "Okay")
|
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
|
||||||
.performClick()
|
|
||||||
verify { viewModel.trySendAction(CreateAccountAction.ErrorDialogDismiss) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking No on the HIBP dialog should send ErrorDialogDismiss action`() {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(dialog = createHaveIBeenPwned())
|
|
||||||
}
|
|
||||||
composeTestRule
|
|
||||||
.onAllNodesWithText("No")
|
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
|
||||||
.performClick()
|
|
||||||
verify { viewModel.trySendAction(CreateAccountAction.ErrorDialogDismiss) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking Yes on the HIBP dialog should send ContinueWithBreachedPasswordClick action`() {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(dialog = createHaveIBeenPwned())
|
|
||||||
}
|
|
||||||
composeTestRule
|
|
||||||
.onAllNodesWithText("Yes")
|
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
|
||||||
.performClick()
|
|
||||||
verify { viewModel.trySendAction(CreateAccountAction.ContinueWithBreachedPasswordClick) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when BasicDialogState is Shown should show dialog`() {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = "title".asText(),
|
|
||||||
message = "message".asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composeTestRule.onNode(isDialog()).assertIsDisplayed()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `password strength should change as state changes`() {
|
|
||||||
mutableStateFlow.update {
|
|
||||||
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_1)
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
|
|
||||||
|
|
||||||
mutableStateFlow.update {
|
|
||||||
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_2)
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
|
|
||||||
|
|
||||||
mutableStateFlow.update {
|
|
||||||
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_3)
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
|
|
||||||
|
|
||||||
mutableStateFlow.update {
|
|
||||||
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.GOOD)
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText("Good").assertIsDisplayed()
|
|
||||||
|
|
||||||
mutableStateFlow.update {
|
|
||||||
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.STRONG)
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText("Strong").assertIsDisplayed()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `toggling one password field visibility should toggle the other`() {
|
|
||||||
// should start with 2 Show buttons:
|
|
||||||
composeTestRule
|
|
||||||
.onAllNodesWithContentDescription("Show")
|
|
||||||
.assertCountEquals(2)[0]
|
|
||||||
.performClick()
|
|
||||||
|
|
||||||
// after clicking there should be no Show buttons:
|
|
||||||
composeTestRule
|
|
||||||
.onAllNodesWithContentDescription("Show")
|
|
||||||
.assertCountEquals(0)
|
|
||||||
|
|
||||||
// and there should be 2 hide buttons now, and we'll click the second one:
|
|
||||||
composeTestRule
|
|
||||||
.onAllNodesWithContentDescription("Hide")
|
|
||||||
.assertCountEquals(2)[1]
|
|
||||||
.performClick()
|
|
||||||
|
|
||||||
// then there should be two show buttons again
|
|
||||||
composeTestRule
|
|
||||||
.onAllNodesWithContentDescription("Show")
|
|
||||||
.assertCountEquals(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `terms of service click should send TermsClick action`() {
|
|
||||||
composeTestRule
|
|
||||||
.onNodeWithText(text = "Terms of Service", substring = true)
|
|
||||||
.performScrollTo()
|
|
||||||
.performCustomAccessibilityAction("Terms of Service")
|
|
||||||
verify { viewModel.trySendAction(CreateAccountAction.TermsClick) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `privacy policy click should send PrivacyPolicyClick action`() {
|
|
||||||
composeTestRule
|
|
||||||
.onNodeWithText(text = "Privacy Policy", substring = true)
|
|
||||||
.performScrollTo()
|
|
||||||
.performCustomAccessibilityAction("Privacy Policy")
|
|
||||||
verify { viewModel.trySendAction(CreateAccountAction.PrivacyPolicyClick) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TEST_INPUT = "input"
|
|
||||||
private val DEFAULT_STATE = CreateAccountState(
|
|
||||||
emailInput = "",
|
|
||||||
passwordInput = "",
|
|
||||||
confirmPasswordInput = "",
|
|
||||||
passwordHintInput = "",
|
|
||||||
isCheckDataBreachesToggled = false,
|
|
||||||
isAcceptPoliciesToggled = false,
|
|
||||||
dialog = null,
|
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
|
||||||
|
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
|
||||||
import com.bitwarden.ui.util.Text
|
|
||||||
import com.bitwarden.ui.util.asText
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a mock [CreateAccountDialog.HaveIBeenPwned].
|
|
||||||
*/
|
|
||||||
fun createHaveIBeenPwned(
|
|
||||||
title: Text = BitwardenString.weak_and_exposed_master_password.asText(),
|
|
||||||
message: Text = BitwardenString
|
|
||||||
.weak_password_identified_and_found_in_a_data_breach_alert_description
|
|
||||||
.asText(),
|
|
||||||
): CreateAccountDialog.HaveIBeenPwned =
|
|
||||||
CreateAccountDialog.HaveIBeenPwned(
|
|
||||||
title = title,
|
|
||||||
message = message,
|
|
||||||
)
|
|
||||||
@ -1,708 +0,0 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
|
||||||
import com.bitwarden.ui.util.asText
|
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_0
|
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_1
|
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_2
|
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_3
|
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4
|
|
||||||
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.ui.auth.feature.completeregistration.PasswordStrengthState
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Internal.ReceivePasswordStrengthResult
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
|
||||||
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.flow.emptyFlow
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
|
||||||
class CreateAccountViewModelTest : BaseViewModelTest() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saved state handle that has valid inputs. Useful for tests that want to test things
|
|
||||||
* after the user has entered all valid inputs.
|
|
||||||
*/
|
|
||||||
private val validInputHandle = SavedStateHandle(mapOf("state" to VALID_INPUT_STATE))
|
|
||||||
|
|
||||||
private val mockAuthRepository = mockk<AuthRepository> {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setUp() {
|
|
||||||
mockkStatic(::generateUriForCaptcha)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
fun tearDown() {
|
|
||||||
unmockkStatic(::generateUriForCaptcha)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `initial state should be correct`() {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `initial state should pull from saved state handle when present`() {
|
|
||||||
val savedState = CreateAccountState(
|
|
||||||
emailInput = "email",
|
|
||||||
passwordInput = "password",
|
|
||||||
confirmPasswordInput = "confirmPassword",
|
|
||||||
passwordHintInput = "hint",
|
|
||||||
isCheckDataBreachesToggled = false,
|
|
||||||
isAcceptPoliciesToggled = false,
|
|
||||||
dialog = null,
|
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
|
||||||
)
|
|
||||||
val handle = SavedStateHandle(mapOf("state" to savedState))
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = handle,
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
assertEquals(savedState, viewModel.stateFlow.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick with blank email should show email required`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
val input = "a"
|
|
||||||
viewModel.trySendAction(EmailInputChange(input))
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
|
||||||
emailInput = input,
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.invalid_email.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(expectedState, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick with invalid email should show invalid email`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
val input = " "
|
|
||||||
viewModel.trySendAction(EmailInputChange(input))
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
|
||||||
emailInput = input,
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.validation_field_required
|
|
||||||
.asText(BitwardenString.email_address.asText()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(expectedState, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick with password below 12 chars should show password length dialog`() = runTest {
|
|
||||||
val input = "abcdefghikl"
|
|
||||||
coEvery {
|
|
||||||
mockAuthRepository.getPasswordStrength("test@test.com", input)
|
|
||||||
} returns PasswordStrengthResult.Error(error = Throwable("Fail!"))
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(EmailInputChange(EMAIL))
|
|
||||||
viewModel.trySendAction(PasswordInputChange(input))
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
|
||||||
emailInput = EMAIL,
|
|
||||||
passwordInput = input,
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.master_password_length_val_message_x.asText(12),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(expectedState, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick with passwords not matching should show password match dialog`() = runTest {
|
|
||||||
val input = "testtesttesttest"
|
|
||||||
coEvery {
|
|
||||||
mockAuthRepository.getPasswordStrength("test@test.com", input)
|
|
||||||
} returns PasswordStrengthResult.Error(error = Throwable("Fail!"))
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(EmailInputChange("test@test.com"))
|
|
||||||
viewModel.trySendAction(PasswordInputChange(input))
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
|
||||||
emailInput = "test@test.com",
|
|
||||||
passwordInput = input,
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.master_password_confirmation_val_message.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(expectedState, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick without policies accepted should show accept policies error`() = runTest {
|
|
||||||
val password = "testtesttesttest"
|
|
||||||
coEvery {
|
|
||||||
mockAuthRepository.getPasswordStrength("test@test.com", password)
|
|
||||||
} returns PasswordStrengthResult.Error(error = Throwable("Fail!"))
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(EmailInputChange("test@test.com"))
|
|
||||||
viewModel.trySendAction(PasswordInputChange(password))
|
|
||||||
viewModel.trySendAction(ConfirmPasswordInputChange(password))
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
|
||||||
emailInput = "test@test.com",
|
|
||||||
passwordInput = password,
|
|
||||||
confirmPasswordInput = password,
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = BitwardenString.accept_policies_error.asText(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(expectedState, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick with all inputs valid should show and hide loading dialog`() = runTest {
|
|
||||||
val repo = mockk<AuthRepository> {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = false,
|
|
||||||
isMasterPasswordStrong = true,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.Success(captchaToken = "mock_token")
|
|
||||||
}
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = validInputHandle,
|
|
||||||
authRepository = repo,
|
|
||||||
)
|
|
||||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
|
||||||
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem())
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
assertEquals(
|
|
||||||
VALID_INPUT_STATE.copy(dialog = CreateAccountDialog.Loading),
|
|
||||||
stateFlow.awaitItem(),
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
CreateAccountEvent.NavigateToLogin(
|
|
||||||
email = EMAIL,
|
|
||||||
captchaToken = "mock_token",
|
|
||||||
),
|
|
||||||
eventFlow.awaitItem(),
|
|
||||||
)
|
|
||||||
// Make sure loading dialog is hidden:
|
|
||||||
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick register returns error should update errorDialogState`() = runTest {
|
|
||||||
val error = Throwable("Fail!")
|
|
||||||
val repo = mockk<AuthRepository> {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = false,
|
|
||||||
isMasterPasswordStrong = true,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.Error(errorMessage = "mock_error", error = error)
|
|
||||||
}
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = validInputHandle,
|
|
||||||
authRepository = repo,
|
|
||||||
)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(VALID_INPUT_STATE, awaitItem())
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
assertEquals(
|
|
||||||
VALID_INPUT_STATE.copy(dialog = CreateAccountDialog.Loading),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
VALID_INPUT_STATE.copy(
|
|
||||||
dialog = CreateAccountDialog.Error(
|
|
||||||
title = BitwardenString.an_error_has_occurred.asText(),
|
|
||||||
message = "mock_error".asText(),
|
|
||||||
error = error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick register returns CaptchaRequired should emit NavigateToCaptcha`() = runTest {
|
|
||||||
val mockkUri = mockk<Uri>()
|
|
||||||
every {
|
|
||||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
|
||||||
} returns mockkUri
|
|
||||||
val repo = mockk<AuthRepository> {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = false,
|
|
||||||
isMasterPasswordStrong = true,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
|
||||||
}
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = validInputHandle,
|
|
||||||
authRepository = repo,
|
|
||||||
)
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
assertEquals(
|
|
||||||
CreateAccountEvent.NavigateToCaptcha(uri = mockkUri),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick register returns Success should emit NavigateToLogin`() = runTest {
|
|
||||||
val mockkUri = mockk<Uri>()
|
|
||||||
every {
|
|
||||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
|
||||||
} returns mockkUri
|
|
||||||
val repo = mockk<AuthRepository> {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = false,
|
|
||||||
isMasterPasswordStrong = true,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.Success(captchaToken = "mock_captcha_token")
|
|
||||||
}
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = validInputHandle,
|
|
||||||
authRepository = repo,
|
|
||||||
)
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
assertEquals(
|
|
||||||
CreateAccountEvent.NavigateToLogin(
|
|
||||||
email = EMAIL,
|
|
||||||
captchaToken = "mock_captcha_token",
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `ContinueWithBreachedPasswordClick should call repository with checkDataBreaches false`() {
|
|
||||||
val repo = mockk<AuthRepository> {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = false,
|
|
||||||
isMasterPasswordStrong = true,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.Error(errorMessage = null, error = null)
|
|
||||||
}
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = validInputHandle,
|
|
||||||
authRepository = repo,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.ContinueWithBreachedPasswordClick)
|
|
||||||
coVerify {
|
|
||||||
repo.register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = false,
|
|
||||||
isMasterPasswordStrong = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SubmitClick register returns ShowDataBreaches should show HaveIBeenPwned dialog`() =
|
|
||||||
runTest {
|
|
||||||
mockAuthRepository.apply {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = true,
|
|
||||||
isMasterPasswordStrong = true,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.DataBreachFound
|
|
||||||
}
|
|
||||||
val initialState = VALID_INPUT_STATE.copy(
|
|
||||||
isCheckDataBreachesToggled = true,
|
|
||||||
)
|
|
||||||
val viewModel = createCreateAccountViewModel(createAccountState = initialState)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(
|
|
||||||
initialState.copy(
|
|
||||||
dialog = createHaveIBeenPwned(
|
|
||||||
title = BitwardenString.exposed_master_password.asText(),
|
|
||||||
message = BitwardenString
|
|
||||||
.password_found_in_a_data_breach_alert_description
|
|
||||||
.asText(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
fun `SubmitClick register returns DataBreachAndWeakPassword should show HaveIBeenPwned dialog`() =
|
|
||||||
runTest {
|
|
||||||
mockAuthRepository.apply {
|
|
||||||
every { captchaTokenResultFlow } returns emptyFlow()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = true,
|
|
||||||
isMasterPasswordStrong = false,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.DataBreachAndWeakPassword
|
|
||||||
}
|
|
||||||
val initialState = VALID_INPUT_STATE.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
|
||||||
isCheckDataBreachesToggled = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
val viewModel = createCreateAccountViewModel(createAccountState = initialState)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(
|
|
||||||
initialState.copy(dialog = createHaveIBeenPwned()),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
fun `SubmitClick register returns WeakPassword should show HaveIBeenPwned dialog`() =
|
|
||||||
runTest {
|
|
||||||
mockAuthRepository.apply {
|
|
||||||
every { captchaTokenResultFlow } returns flowOf()
|
|
||||||
coEvery {
|
|
||||||
register(
|
|
||||||
email = EMAIL,
|
|
||||||
masterPassword = PASSWORD,
|
|
||||||
masterPasswordHint = null,
|
|
||||||
captchaToken = null,
|
|
||||||
shouldCheckDataBreaches = true,
|
|
||||||
isMasterPasswordStrong = false,
|
|
||||||
)
|
|
||||||
} returns RegisterResult.WeakPassword
|
|
||||||
}
|
|
||||||
val initialState = VALID_INPUT_STATE
|
|
||||||
.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
|
||||||
isCheckDataBreachesToggled = true,
|
|
||||||
)
|
|
||||||
val viewModel = createCreateAccountViewModel(createAccountState = initialState)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(
|
|
||||||
initialState.copy(
|
|
||||||
dialog = createHaveIBeenPwned(
|
|
||||||
title = BitwardenString.weak_master_password.asText(),
|
|
||||||
message = BitwardenString.weak_password_identified_use_a_strong_password_to_protect_your_account.asText(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `CloseClick should emit NavigateBack`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
viewModel.trySendAction(CloseClick)
|
|
||||||
assertEquals(CreateAccountEvent.NavigateBack, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `PrivacyPolicyClick should emit NavigatePrivacyPolicy`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
viewModel.trySendAction(CreateAccountAction.PrivacyPolicyClick)
|
|
||||||
assertEquals(CreateAccountEvent.NavigateToPrivacyPolicy, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `TermsClick should emit NavigateToTerms`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
viewModel.trySendAction(CreateAccountAction.TermsClick)
|
|
||||||
assertEquals(CreateAccountEvent.NavigateToTerms, awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `ConfirmPasswordInputChange update passwordInput`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(ConfirmPasswordInputChange("input"))
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(DEFAULT_STATE.copy(confirmPasswordInput = "input"), awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `EmailInputChange update passwordInput`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(EmailInputChange("input"))
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(DEFAULT_STATE.copy(emailInput = "input"), awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `PasswordHintChange update passwordInput`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(PasswordHintChange("input"))
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(DEFAULT_STATE.copy(passwordHintInput = "input"), awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `PasswordInputChange update passwordInput and call getPasswordStrength`() = runTest {
|
|
||||||
coEvery {
|
|
||||||
mockAuthRepository.getPasswordStrength("", "input")
|
|
||||||
} returns PasswordStrengthResult.Error(error = Throwable("Fail!"))
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(PasswordInputChange("input"))
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(DEFAULT_STATE.copy(passwordInput = "input"), awaitItem())
|
|
||||||
}
|
|
||||||
coVerify { mockAuthRepository.getPasswordStrength("", "input") }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `CheckDataBreachesToggle should change isCheckDataBreachesToggled`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(CreateAccountAction.CheckDataBreachesToggle(true))
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(DEFAULT_STATE.copy(isCheckDataBreachesToggled = true), awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `AcceptPoliciesToggle should change isAcceptPoliciesToggled`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(AcceptPoliciesToggle(true))
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(DEFAULT_STATE.copy(isAcceptPoliciesToggled = true), awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `ReceivePasswordStrengthResult should update password strength state`() = runTest {
|
|
||||||
val viewModel = CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
authRepository = mockAuthRepository,
|
|
||||||
)
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(
|
|
||||||
DEFAULT_STATE.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.trySendAction(
|
|
||||||
ReceivePasswordStrengthResult(PasswordStrengthResult.Success(LEVEL_0)),
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
DEFAULT_STATE.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.trySendAction(
|
|
||||||
ReceivePasswordStrengthResult(PasswordStrengthResult.Success(LEVEL_1)),
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
DEFAULT_STATE.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.WEAK_2,
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.trySendAction(
|
|
||||||
ReceivePasswordStrengthResult(PasswordStrengthResult.Success(LEVEL_2)),
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
DEFAULT_STATE.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.WEAK_3,
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.trySendAction(
|
|
||||||
ReceivePasswordStrengthResult(PasswordStrengthResult.Success(LEVEL_3)),
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
DEFAULT_STATE.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.GOOD,
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.trySendAction(
|
|
||||||
ReceivePasswordStrengthResult(PasswordStrengthResult.Success(LEVEL_4)),
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
DEFAULT_STATE.copy(
|
|
||||||
passwordStrengthState = PasswordStrengthState.STRONG,
|
|
||||||
),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCreateAccountViewModel(
|
|
||||||
createAccountState: CreateAccountState? = null,
|
|
||||||
authRepository: AuthRepository = mockAuthRepository,
|
|
||||||
): CreateAccountViewModel =
|
|
||||||
CreateAccountViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(mapOf("state" to createAccountState)),
|
|
||||||
authRepository = authRepository,
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PASSWORD = "longenoughtpassword"
|
|
||||||
private const val EMAIL = "test@test.com"
|
|
||||||
private val DEFAULT_STATE = CreateAccountState(
|
|
||||||
passwordInput = "",
|
|
||||||
emailInput = "",
|
|
||||||
confirmPasswordInput = "",
|
|
||||||
passwordHintInput = "",
|
|
||||||
isCheckDataBreachesToggled = true,
|
|
||||||
isAcceptPoliciesToggled = false,
|
|
||||||
dialog = null,
|
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
|
||||||
)
|
|
||||||
private val VALID_INPUT_STATE = CreateAccountState(
|
|
||||||
passwordInput = PASSWORD,
|
|
||||||
emailInput = EMAIL,
|
|
||||||
confirmPasswordInput = PASSWORD,
|
|
||||||
passwordHintInput = "",
|
|
||||||
isCheckDataBreachesToggled = false,
|
|
||||||
isAcceptPoliciesToggled = true,
|
|
||||||
dialog = null,
|
|
||||||
passwordStrengthState = PasswordStrengthState.GOOD,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -47,7 +47,6 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||||||
|
|
||||||
class LandingScreenTest : BitwardenComposeTest() {
|
class LandingScreenTest : BitwardenComposeTest() {
|
||||||
private var capturedEmail: String? = null
|
private var capturedEmail: String? = null
|
||||||
private var onNavigateToCreateAccountCalled = false
|
|
||||||
private var onNavigateToLoginCalled = false
|
private var onNavigateToLoginCalled = false
|
||||||
private var onNavigateToEnvironmentCalled = false
|
private var onNavigateToEnvironmentCalled = false
|
||||||
private var onNavigateToStartRegistrationCalled = false
|
private var onNavigateToStartRegistrationCalled = false
|
||||||
@ -64,7 +63,6 @@ class LandingScreenTest : BitwardenComposeTest() {
|
|||||||
fun setUp() {
|
fun setUp() {
|
||||||
setContent {
|
setContent {
|
||||||
LandingScreen(
|
LandingScreen(
|
||||||
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
|
|
||||||
onNavigateToLogin = { capturedEmail ->
|
onNavigateToLogin = { capturedEmail ->
|
||||||
this.capturedEmail = capturedEmail
|
this.capturedEmail = capturedEmail
|
||||||
onNavigateToLoginCalled = true
|
onNavigateToLoginCalled = true
|
||||||
@ -316,12 +314,6 @@ class LandingScreenTest : BitwardenComposeTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateToCreateAccount event should call onNavigateToCreateAccount`() {
|
|
||||||
mutableEventFlow.tryEmit(LandingEvent.NavigateToCreateAccount)
|
|
||||||
assertTrue(onNavigateToCreateAccountCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
||||||
val testEmail = "test@test.com"
|
val testEmail = "test@test.com"
|
||||||
|
|||||||
@ -12,9 +12,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
|||||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
@ -49,9 +47,6 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||||||
getSnackbarDataFlow(SnackbarRelay.ENVIRONMENT_SAVED)
|
getSnackbarDataFlow(SnackbarRelay.ENVIRONMENT_SAVED)
|
||||||
} returns mutableSnackbarSharedFlow
|
} returns mutableSnackbarSharedFlow
|
||||||
}
|
}
|
||||||
private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) {
|
|
||||||
every { getFeatureFlag(FlagKey.EmailVerification) } returns false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct when there is no remembered email`() = runTest {
|
fun `initial state should be correct when there is no remembered email`() = runTest {
|
||||||
@ -405,31 +400,17 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CreateAccountClick should emit NavigateToCreateAccount`() = runTest {
|
fun `CreateAccountClick should emit NavigateToStartRegistration`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(LandingAction.CreateAccountClick)
|
viewModel.trySendAction(LandingAction.CreateAccountClick)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
LandingEvent.NavigateToCreateAccount,
|
LandingEvent.NavigateToStartRegistration,
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `When feature is enabled CreateAccountClick should emit NavigateToStartRegistration`() =
|
|
||||||
runTest {
|
|
||||||
every { featureFlagManager.getFeatureFlag(FlagKey.EmailVerification) } returns true
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
viewModel.trySendAction(LandingAction.CreateAccountClick)
|
|
||||||
assertEquals(
|
|
||||||
LandingEvent.NavigateToStartRegistration,
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `DialogDismiss should clear the active dialog`() {
|
fun `DialogDismiss should clear the active dialog`() {
|
||||||
val initialState = DEFAULT_STATE.copy(
|
val initialState = DEFAULT_STATE.copy(
|
||||||
@ -623,7 +604,6 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||||||
},
|
},
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
environmentRepository = fakeEnvironmentRepository,
|
environmentRepository = fakeEnvironmentRepository,
|
||||||
featureFlagManager = featureFlagManager,
|
|
||||||
snackbarRelayManager = snackbarRelayManager,
|
snackbarRelayManager = snackbarRelayManager,
|
||||||
savedStateHandle = savedStateHandle,
|
savedStateHandle = savedStateHandle,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import org.robolectric.annotation.Config
|
|||||||
|
|
||||||
class WelcomeScreenTest : BitwardenComposeTest() {
|
class WelcomeScreenTest : BitwardenComposeTest() {
|
||||||
private var onNavigateToStartRegistrationCalled = false
|
private var onNavigateToStartRegistrationCalled = false
|
||||||
private var onNavigateToCreateAccountCalled = false
|
|
||||||
private var onNavigateToLoginCalled = false
|
private var onNavigateToLoginCalled = false
|
||||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<WelcomeEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<WelcomeEvent>()
|
||||||
@ -30,7 +29,6 @@ class WelcomeScreenTest : BitwardenComposeTest() {
|
|||||||
fun setUp() {
|
fun setUp() {
|
||||||
setContent {
|
setContent {
|
||||||
WelcomeScreen(
|
WelcomeScreen(
|
||||||
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
|
|
||||||
onNavigateToLogin = { onNavigateToLoginCalled = true },
|
onNavigateToLogin = { onNavigateToLoginCalled = true },
|
||||||
onNavigateToStartRegistration = { onNavigateToStartRegistrationCalled = true },
|
onNavigateToStartRegistration = { onNavigateToStartRegistrationCalled = true },
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@ -85,12 +83,6 @@ class WelcomeScreenTest : BitwardenComposeTest() {
|
|||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateToCreateAccount event should call onNavigateToCreateAccount`() {
|
|
||||||
mutableEventFlow.tryEmit(WelcomeEvent.NavigateToCreateAccount)
|
|
||||||
assertTrue(onNavigateToCreateAccountCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
||||||
mutableEventFlow.tryEmit(WelcomeEvent.NavigateToLogin)
|
mutableEventFlow.tryEmit(WelcomeEvent.NavigateToLogin)
|
||||||
|
|||||||
@ -2,21 +2,15 @@ package com.x8bit.bitwarden.ui.auth.feature.welcome
|
|||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class WelcomeViewModelTest : BaseViewModelTest() {
|
class WelcomeViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
private val featureFlagManager = mockk<FeatureFlagManager>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct`() = runTest {
|
fun `initial state should be correct`() = runTest {
|
||||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.stateFlow.test {
|
viewModel.stateFlow.test {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@ -28,7 +22,7 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `PagerSwipe should update state`() = runTest {
|
fun `PagerSwipe should update state`() = runTest {
|
||||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
val viewModel = createViewModel()
|
||||||
val newIndex = 2
|
val newIndex = 2
|
||||||
|
|
||||||
viewModel.trySendAction(WelcomeAction.PagerSwipe(index = newIndex))
|
viewModel.trySendAction(WelcomeAction.PagerSwipe(index = newIndex))
|
||||||
@ -43,7 +37,7 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `DotClick should update state and emit UpdatePager`() = runTest {
|
fun `DotClick should update state and emit UpdatePager`() = runTest {
|
||||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
val viewModel = createViewModel()
|
||||||
val newIndex = 2
|
val newIndex = 2
|
||||||
|
|
||||||
viewModel.trySendAction(WelcomeAction.DotClick(index = newIndex))
|
viewModel.trySendAction(WelcomeAction.DotClick(index = newIndex))
|
||||||
@ -62,41 +56,22 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
@Test
|
@Test
|
||||||
fun `CreateAccountClick should emit NavigateToCreateAccount when email verification is disabled`() =
|
fun `CreateAccountClick should emit NavigateToStartRegistration`() = runTest {
|
||||||
runTest {
|
val viewModel = createViewModel()
|
||||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
viewModel.trySendAction(WelcomeAction.CreateAccountClick)
|
||||||
every { featureFlagManager.getFeatureFlag(FlagKey.EmailVerification) } returns false
|
|
||||||
viewModel.trySendAction(WelcomeAction.CreateAccountClick)
|
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
WelcomeEvent.NavigateToCreateAccount,
|
WelcomeEvent.NavigateToStartRegistration,
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
@Test
|
|
||||||
fun `CreateAccountClick should emit NavigateToStartRegistration when email verification is enabled`() =
|
|
||||||
runTest {
|
|
||||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
|
||||||
every { featureFlagManager.getFeatureFlag(FlagKey.EmailVerification) } returns true
|
|
||||||
viewModel.trySendAction(WelcomeAction.CreateAccountClick)
|
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
assertEquals(
|
|
||||||
WelcomeEvent.NavigateToStartRegistration,
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `LoginClick should emit NavigateToLogin`() = runTest {
|
fun `LoginClick should emit NavigateToLogin`() = runTest {
|
||||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.trySendAction(WelcomeAction.LoginClick)
|
viewModel.trySendAction(WelcomeAction.LoginClick)
|
||||||
|
|
||||||
@ -107,6 +82,8 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(): WelcomeViewModel = WelcomeViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_STATE = WelcomeState(
|
private val DEFAULT_STATE = WelcomeState(
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
|||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performScrollTo
|
import androidx.compose.ui.test.performScrollTo
|
||||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
import com.bitwarden.core.data.manager.model.FlagKey
|
||||||
|
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
@ -85,13 +85,13 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
|||||||
mutableStateFlow.tryEmit(
|
mutableStateFlow.tryEmit(
|
||||||
DebugMenuState(
|
DebugMenuState(
|
||||||
featureFlags = persistentMapOf(
|
featureFlags = persistentMapOf(
|
||||||
FlagKey.EmailVerification to true,
|
FlagKey.CredentialExchangeProtocolImport to true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Email Verification", ignoreCase = true)
|
.onNodeWithText("CXP Import", ignoreCase = true)
|
||||||
.assertExists()
|
.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,18 +100,18 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
|||||||
mutableStateFlow.tryEmit(
|
mutableStateFlow.tryEmit(
|
||||||
DebugMenuState(
|
DebugMenuState(
|
||||||
featureFlags = persistentMapOf(
|
featureFlags = persistentMapOf(
|
||||||
FlagKey.EmailVerification to true,
|
FlagKey.CredentialExchangeProtocolImport to true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Email Verification", ignoreCase = true)
|
.onNodeWithText("CXP Import", ignoreCase = true)
|
||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
DebugMenuAction.UpdateFeatureFlag(
|
DebugMenuAction.UpdateFeatureFlag(
|
||||||
FlagKey.EmailVerification,
|
FlagKey.CredentialExchangeProtocolImport,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package com.x8bit.bitwarden.ui.platform.feature.debugmenu
|
package com.x8bit.bitwarden.ui.platform.feature.debugmenu
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.bitwarden.core.data.manager.model.FlagKey
|
||||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||||
import com.bitwarden.core.data.manager.model.FlagKey
|
|
||||||
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
||||||
import com.x8bit.bitwarden.data.util.assertCoroutineThrows
|
import com.x8bit.bitwarden.data.util.assertCoroutineThrows
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
@ -101,10 +101,10 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
|||||||
fun `handleUpdateFeatureFlag should update the feature flag via the repository`() {
|
fun `handleUpdateFeatureFlag should update the feature flag via the repository`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
DebugMenuAction.UpdateFeatureFlag(FlagKey.EmailVerification, false),
|
DebugMenuAction.UpdateFeatureFlag(FlagKey.CipherKeyEncryption, false),
|
||||||
)
|
)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
mockDebugMenuRepository.updateFeatureFlag(FlagKey.EmailVerification, false)
|
mockDebugMenuRepository.updateFeatureFlag(FlagKey.CipherKeyEncryption, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +144,6 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf(
|
private val DEFAULT_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf(
|
||||||
FlagKey.EmailVerification to true,
|
|
||||||
FlagKey.CredentialExchangeProtocolImport to true,
|
FlagKey.CredentialExchangeProtocolImport to true,
|
||||||
FlagKey.CredentialExchangeProtocolExport to true,
|
FlagKey.CredentialExchangeProtocolExport to true,
|
||||||
FlagKey.RestrictCipherItemDeletion to true,
|
FlagKey.RestrictCipherItemDeletion to true,
|
||||||
@ -153,7 +152,6 @@ private val DEFAULT_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf
|
|||||||
)
|
)
|
||||||
|
|
||||||
private val UPDATED_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf(
|
private val UPDATED_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf(
|
||||||
FlagKey.EmailVerification to false,
|
|
||||||
FlagKey.CredentialExchangeProtocolImport to false,
|
FlagKey.CredentialExchangeProtocolImport to false,
|
||||||
FlagKey.CredentialExchangeProtocolExport to false,
|
FlagKey.CredentialExchangeProtocolExport to false,
|
||||||
FlagKey.RestrictCipherItemDeletion to false,
|
FlagKey.RestrictCipherItemDeletion to false,
|
||||||
|
|||||||
@ -26,7 +26,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
|||||||
FlagKey.CipherKeyEncryption,
|
FlagKey.CipherKeyEncryption,
|
||||||
FlagKey.CredentialExchangeProtocolExport,
|
FlagKey.CredentialExchangeProtocolExport,
|
||||||
FlagKey.CredentialExchangeProtocolImport,
|
FlagKey.CredentialExchangeProtocolImport,
|
||||||
FlagKey.EmailVerification,
|
|
||||||
FlagKey.RemoveCardPolicy,
|
FlagKey.RemoveCardPolicy,
|
||||||
FlagKey.RestrictCipherItemDeletion,
|
FlagKey.RestrictCipherItemDeletion,
|
||||||
FlagKey.UserManagedPrivilegedApps,
|
FlagKey.UserManagedPrivilegedApps,
|
||||||
@ -67,7 +66,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
|||||||
FlagKey.DummyString,
|
FlagKey.DummyString,
|
||||||
-> this.keyName
|
-> this.keyName
|
||||||
|
|
||||||
FlagKey.EmailVerification -> stringResource(BitwardenString.email_verification)
|
|
||||||
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
||||||
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
||||||
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
||||||
|
|||||||
@ -30,7 +30,6 @@ sealed class FlagKey<out T : Any> {
|
|||||||
*/
|
*/
|
||||||
val activePasswordManagerFlags: List<FlagKey<*>> by lazy {
|
val activePasswordManagerFlags: List<FlagKey<*>> by lazy {
|
||||||
listOf(
|
listOf(
|
||||||
EmailVerification,
|
|
||||||
CredentialExchangeProtocolImport,
|
CredentialExchangeProtocolImport,
|
||||||
CredentialExchangeProtocolExport,
|
CredentialExchangeProtocolExport,
|
||||||
RestrictCipherItemDeletion,
|
RestrictCipherItemDeletion,
|
||||||
@ -40,14 +39,6 @@ sealed class FlagKey<out T : Any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object holding the key for Email Verification feature.
|
|
||||||
*/
|
|
||||||
data object EmailVerification : FlagKey<Boolean>() {
|
|
||||||
override val keyName: String = "email-verification"
|
|
||||||
override val defaultValue: Boolean = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data object holding hte feature flag key for the Credential Exchange Protocol (CXP) import
|
* Data object holding hte feature flag key for the Credential Exchange Protocol (CXP) import
|
||||||
* feature.
|
* feature.
|
||||||
|
|||||||
@ -8,10 +8,6 @@ class FlagKeyTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Feature flags have the correct key name set`() {
|
fun `Feature flags have the correct key name set`() {
|
||||||
assertEquals(
|
|
||||||
FlagKey.EmailVerification.keyName,
|
|
||||||
"email-verification",
|
|
||||||
)
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
FlagKey.CredentialExchangeProtocolImport.keyName,
|
FlagKey.CredentialExchangeProtocolImport.keyName,
|
||||||
"cxp-import-mobile",
|
"cxp-import-mobile",
|
||||||
@ -46,7 +42,6 @@ class FlagKeyTest {
|
|||||||
fun `All feature flags have the correct default value set`() {
|
fun `All feature flags have the correct default value set`() {
|
||||||
assertTrue(
|
assertTrue(
|
||||||
listOf(
|
listOf(
|
||||||
FlagKey.EmailVerification,
|
|
||||||
FlagKey.CredentialExchangeProtocolImport,
|
FlagKey.CredentialExchangeProtocolImport,
|
||||||
FlagKey.CredentialExchangeProtocolExport,
|
FlagKey.CredentialExchangeProtocolExport,
|
||||||
FlagKey.CipherKeyEncryption,
|
FlagKey.CipherKeyEncryption,
|
||||||
|
|||||||
@ -420,7 +420,6 @@ Scanning will happen automatically.</string>
|
|||||||
<string name="master_password_policy_validation_title">Invalid password</string>
|
<string name="master_password_policy_validation_title">Invalid password</string>
|
||||||
<string name="master_password_policy_validation_message">Password does not meet organization requirements. Please check the policy information and try again.</string>
|
<string name="master_password_policy_validation_message">Password does not meet organization requirements. Please check the policy information and try again.</string>
|
||||||
<string name="loading">Loading</string>
|
<string name="loading">Loading</string>
|
||||||
<string name="accept_policies_error">Terms of Service and Privacy Policy have not been acknowledged.</string>
|
|
||||||
<string name="terms_of_service">Terms of Service</string>
|
<string name="terms_of_service">Terms of Service</string>
|
||||||
<string name="privacy_policy">Privacy Policy</string>
|
<string name="privacy_policy">Privacy Policy</string>
|
||||||
<string name="passkey_management">Passkey management</string>
|
<string name="passkey_management">Passkey management</string>
|
||||||
@ -609,8 +608,6 @@ Do you want to switch to this account?</string>
|
|||||||
<string name="language">Language</string>
|
<string name="language">Language</string>
|
||||||
<string name="language_change_x_description">The language has been changed to %1$s. Please restart the app to see the change</string>
|
<string name="language_change_x_description">The language has been changed to %1$s. Please restart the app to see the change</string>
|
||||||
<string name="default_system">Default (System)</string>
|
<string name="default_system">Default (System)</string>
|
||||||
<string name="important">Important</string>
|
|
||||||
<string name="your_master_password_cannot_be_recovered_if_you_forget_it_x_characters_minimum">Your master password cannot be recovered if you forget it! %1$s characters minimum.</string>
|
|
||||||
<string name="weak_master_password">Weak Master Password</string>
|
<string name="weak_master_password">Weak Master Password</string>
|
||||||
<string name="weak_password_identified_use_a_strong_password_to_protect_your_account">Weak password identified. Use a strong password to protect your account. Are you sure you want to use a weak password?</string>
|
<string name="weak_password_identified_use_a_strong_password_to_protect_your_account">Weak password identified. Use a strong password to protect your account. Are you sure you want to use a weak password?</string>
|
||||||
<string name="weak">Weak</string>
|
<string name="weak">Weak</string>
|
||||||
@ -707,7 +704,6 @@ Do you want to switch to this account?</string>
|
|||||||
<string name="create_account_on_with_colon">Create account on:</string>
|
<string name="create_account_on_with_colon">Create account on:</string>
|
||||||
<string name="we_sent_an_email_to">We sent an email to <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>.</string>
|
<string name="we_sent_an_email_to">We sent an email to <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>.</string>
|
||||||
<string name="by_continuing_you_agree_to_the_terms_of_service_and_privacy_policy">By continuing, you agree to the <annotation link="termsOfService">Terms of Service</annotation> and <annotation link="privacyPolicy">Privacy Policy</annotation></string>
|
<string name="by_continuing_you_agree_to_the_terms_of_service_and_privacy_policy">By continuing, you agree to the <annotation link="termsOfService">Terms of Service</annotation> and <annotation link="privacyPolicy">Privacy Policy</annotation></string>
|
||||||
<string name="by_activating_this_switch_you_agree_to_the_terms_of_service_and_privacy_policy">By activating this switch, you agree to the <annotation link="termsOfService">Terms of Service</annotation> and <annotation link="privacyPolicy">Privacy Policy</annotation></string>
|
|
||||||
<string name="unsubscribe">Unsubscribe</string>
|
<string name="unsubscribe">Unsubscribe</string>
|
||||||
<string name="check_your_email">Check your email</string>
|
<string name="check_your_email">Check your email</string>
|
||||||
<string name="open_email_app">Open email app</string>
|
<string name="open_email_app">Open email app</string>
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
<string name="json_extension_formatted" translatable="false">.json (%1$s)</string>
|
<string name="json_extension_formatted" translatable="false">.json (%1$s)</string>
|
||||||
|
|
||||||
<!-- region Debug Menu -->
|
<!-- region Debug Menu -->
|
||||||
<string name="email_verification">Email Verification</string>
|
|
||||||
<string name="feature_flags">Feature Flags:</string>
|
<string name="feature_flags">Feature Flags:</string>
|
||||||
<string name="debug_menu">Debug Menu</string>
|
<string name="debug_menu">Debug Menu</string>
|
||||||
<string name="reset_values">Reset values</string>
|
<string name="reset_values">Reset values</string>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user