mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -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.navigateToCompleteRegistration
|
||||
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.navigateToEnterpriseSignOn
|
||||
import com.x8bit.bitwarden.ui.auth.feature.environment.environmentDestination
|
||||
@ -61,18 +59,6 @@ fun NavGraphBuilder.authGraph(
|
||||
navigation<AuthGraphRoute>(
|
||||
startDestination = LandingRoute,
|
||||
) {
|
||||
createAccountDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToLogin = { emailAddress, captchaToken ->
|
||||
navController.navigateToLogin(
|
||||
emailAddress = emailAddress,
|
||||
captchaToken = captchaToken,
|
||||
navOptions = navOptions {
|
||||
popUpTo(route = LandingRoute)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
startRegistrationDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToCompleteRegistration = { emailAddress, verificationToken ->
|
||||
@ -121,7 +107,6 @@ fun NavGraphBuilder.authGraph(
|
||||
)
|
||||
setPasswordDestination()
|
||||
landingDestination(
|
||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
||||
onNavigateToLogin = { emailAddress ->
|
||||
navController.navigateToLogin(
|
||||
emailAddress = emailAddress,
|
||||
@ -135,7 +120,6 @@ fun NavGraphBuilder.authGraph(
|
||||
onNavigateToPreAuthSettings = { navController.navigateToPreAuthSettings() },
|
||||
)
|
||||
welcomeDestination(
|
||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
||||
onNavigateToLogin = { navController.navigateToLanding() },
|
||||
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.
|
||||
*/
|
||||
fun NavGraphBuilder.landingDestination(
|
||||
onNavigateToCreateAccount: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String) -> Unit,
|
||||
onNavigateToEnvironment: () -> Unit,
|
||||
onNavigateToStartRegistration: () -> Unit,
|
||||
@ -31,7 +30,6 @@ fun NavGraphBuilder.landingDestination(
|
||||
) {
|
||||
composableWithStayTransitions<LandingRoute> {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = onNavigateToCreateAccount,
|
||||
onNavigateToLogin = onNavigateToLogin,
|
||||
onNavigateToEnvironment = onNavigateToEnvironment,
|
||||
onNavigateToStartRegistration = onNavigateToStartRegistration,
|
||||
|
||||
@ -65,7 +65,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun LandingScreen(
|
||||
onNavigateToCreateAccount: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String) -> Unit,
|
||||
onNavigateToEnvironment: () -> Unit,
|
||||
onNavigateToStartRegistration: () -> Unit,
|
||||
@ -76,7 +75,6 @@ fun LandingScreen(
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
LandingEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
|
||||
is LandingEvent.NavigateToLogin -> onNavigateToLogin(event.emailAddress)
|
||||
LandingEvent.NavigateToEnvironment -> onNavigateToEnvironment()
|
||||
LandingEvent.NavigateToStartRegistration -> onNavigateToStartRegistration()
|
||||
|
||||
@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.ui.platform.base.BackgroundEvent
|
||||
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.model.LogoutReason
|
||||
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.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
@ -41,7 +39,6 @@ class LandingViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
snackbarRelayManager: SnackbarRelayManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LandingState, LandingEvent, LandingAction>(
|
||||
@ -215,13 +212,7 @@ class LandingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleCreateAccountClicked() {
|
||||
val navigationEvent =
|
||||
if (featureFlagManager.getFeatureFlag(key = FlagKey.EmailVerification)) {
|
||||
LandingEvent.NavigateToStartRegistration
|
||||
} else {
|
||||
LandingEvent.NavigateToCreateAccount
|
||||
}
|
||||
sendEvent(navigationEvent)
|
||||
sendEvent(LandingEvent.NavigateToStartRegistration)
|
||||
}
|
||||
|
||||
private fun handleDialogDismiss() {
|
||||
@ -326,11 +317,6 @@ data class LandingState(
|
||||
* Models events for the landing screen.
|
||||
*/
|
||||
sealed class LandingEvent {
|
||||
/**
|
||||
* Navigates to the Create Account screen.
|
||||
*/
|
||||
data object NavigateToCreateAccount : LandingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the Start Registration screen.
|
||||
*/
|
||||
|
||||
@ -23,13 +23,11 @@ fun NavController.navigateToWelcome(navOptions: NavOptions? = null) {
|
||||
* Add the Welcome screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.welcomeDestination(
|
||||
onNavigateToCreateAccount: () -> Unit,
|
||||
onNavigateToLogin: () -> Unit,
|
||||
onNavigateToStartRegistration: () -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions<WelcomeRoute> {
|
||||
WelcomeScreen(
|
||||
onNavigateToCreateAccount = onNavigateToCreateAccount,
|
||||
onNavigateToLogin = onNavigateToLogin,
|
||||
onNavigateToStartRegistration = onNavigateToStartRegistration,
|
||||
)
|
||||
|
||||
@ -63,7 +63,6 @@ private val HORIZONTAL_MARGIN_MEDIUM: Dp = 128.dp
|
||||
*/
|
||||
@Composable
|
||||
fun WelcomeScreen(
|
||||
onNavigateToCreateAccount: () -> Unit,
|
||||
onNavigateToLogin: () -> Unit,
|
||||
onNavigateToStartRegistration: () -> Unit,
|
||||
viewModel: WelcomeViewModel = hiltViewModel(),
|
||||
@ -78,7 +77,6 @@ fun WelcomeScreen(
|
||||
scope.launch { pagerState.animateScrollToPage(event.index) }
|
||||
}
|
||||
|
||||
WelcomeEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
|
||||
WelcomeEvent.NavigateToLogin -> onNavigateToLogin()
|
||||
WelcomeEvent.NavigateToStartRegistration -> onNavigateToStartRegistration()
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.welcome
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@ -15,9 +13,7 @@ import javax.inject.Inject
|
||||
* Manages application state for the welcome screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class WelcomeViewModel @Inject constructor(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) :
|
||||
class WelcomeViewModel @Inject constructor() :
|
||||
BaseViewModel<WelcomeState, WelcomeEvent, WelcomeAction>(
|
||||
initialState = WelcomeState(
|
||||
index = 0,
|
||||
@ -48,12 +44,7 @@ class WelcomeViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleCreateAccountClick() {
|
||||
val event = if (featureFlagManager.getFeatureFlag(FlagKey.EmailVerification)) {
|
||||
WelcomeEvent.NavigateToStartRegistration
|
||||
} else {
|
||||
WelcomeEvent.NavigateToCreateAccount
|
||||
}
|
||||
sendEvent(event)
|
||||
sendEvent(WelcomeEvent.NavigateToStartRegistration)
|
||||
}
|
||||
|
||||
private fun handleLoginClick() {
|
||||
@ -130,11 +121,6 @@ sealed class WelcomeEvent {
|
||||
val index: Int,
|
||||
) : WelcomeEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the create account screen.
|
||||
*/
|
||||
data object NavigateToCreateAccount : WelcomeEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the login screen.
|
||||
*/
|
||||
|
||||
@ -263,7 +263,7 @@ private fun FeatureFlagContent_preview() {
|
||||
BitwardenTheme {
|
||||
FeatureFlagContent(
|
||||
featureFlagMap = persistentMapOf(
|
||||
FlagKey.EmailVerification to true,
|
||||
FlagKey.DummyBoolean to true,
|
||||
),
|
||||
onValueChange = { _, _ -> },
|
||||
onResetValues = { },
|
||||
|
||||
@ -26,7 +26,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
||||
}
|
||||
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
FlagKey.EmailVerification,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
@ -74,7 +73,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
FlagKey.DummyString,
|
||||
-> this.keyName
|
||||
|
||||
FlagKey.EmailVerification -> stringResource(BitwardenString.email_verification)
|
||||
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
||||
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
||||
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.data.datasource.disk.model.ServerConfig
|
||||
import com.bitwarden.network.model.ConfigResponseJson
|
||||
import com.bitwarden.network.model.ConfigResponseJson.EnvironmentJson
|
||||
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.util.isServerVersionAtLeast
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@ -130,7 +130,7 @@ class FeatureFlagManagerTest {
|
||||
)
|
||||
|
||||
val flagValue = manager.getFeatureFlag(
|
||||
key = FlagKey.EmailVerification,
|
||||
key = FlagKey.DummyBoolean,
|
||||
forceRefresh = false,
|
||||
)
|
||||
assertFalse(flagValue)
|
||||
@ -226,7 +226,7 @@ class FeatureFlagManagerTest {
|
||||
fakeServerConfigRepository.serverConfigValue = null
|
||||
|
||||
val flagValue = manager.getFeatureFlag(
|
||||
key = FlagKey.EmailVerification,
|
||||
key = FlagKey.DummyBoolean,
|
||||
forceRefresh = false,
|
||||
)
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
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.repository.ServerConfigRepository
|
||||
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.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
@ -99,8 +99,8 @@ class DebugMenuRepositoryTest {
|
||||
debugMenuRepository.resetFeatureFlagOverrides()
|
||||
verify(exactly = 1) {
|
||||
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
||||
FlagKey.EmailVerification,
|
||||
FlagKey.EmailVerification.defaultValue,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
FlagKey.CredentialExchangeProtocolImport.defaultValue,
|
||||
)
|
||||
}
|
||||
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() {
|
||||
private var capturedEmail: String? = null
|
||||
private var onNavigateToCreateAccountCalled = false
|
||||
private var onNavigateToLoginCalled = false
|
||||
private var onNavigateToEnvironmentCalled = false
|
||||
private var onNavigateToStartRegistrationCalled = false
|
||||
@ -64,7 +63,6 @@ class LandingScreenTest : BitwardenComposeTest() {
|
||||
fun setUp() {
|
||||
setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
|
||||
onNavigateToLogin = { capturedEmail ->
|
||||
this.capturedEmail = capturedEmail
|
||||
onNavigateToLoginCalled = true
|
||||
@ -316,12 +314,6 @@ class LandingScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCreateAccount event should call onNavigateToCreateAccount`() {
|
||||
mutableEventFlow.tryEmit(LandingEvent.NavigateToCreateAccount)
|
||||
assertTrue(onNavigateToCreateAccountCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
||||
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.UserState
|
||||
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.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
@ -49,9 +47,6 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
getSnackbarDataFlow(SnackbarRelay.ENVIRONMENT_SAVED)
|
||||
} returns mutableSnackbarSharedFlow
|
||||
}
|
||||
private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) {
|
||||
every { getFeatureFlag(FlagKey.EmailVerification) } returns false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when there is no remembered email`() = runTest {
|
||||
@ -405,31 +400,17 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick should emit NavigateToCreateAccount`() = runTest {
|
||||
fun `CreateAccountClick should emit NavigateToStartRegistration`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LandingAction.CreateAccountClick)
|
||||
assertEquals(
|
||||
LandingEvent.NavigateToCreateAccount,
|
||||
LandingEvent.NavigateToStartRegistration,
|
||||
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
|
||||
fun `DialogDismiss should clear the active dialog`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
@ -623,7 +604,6 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
},
|
||||
vaultRepository = vaultRepository,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
@ -17,7 +17,6 @@ import org.robolectric.annotation.Config
|
||||
|
||||
class WelcomeScreenTest : BitwardenComposeTest() {
|
||||
private var onNavigateToStartRegistrationCalled = false
|
||||
private var onNavigateToCreateAccountCalled = false
|
||||
private var onNavigateToLoginCalled = false
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<WelcomeEvent>()
|
||||
@ -30,7 +29,6 @@ class WelcomeScreenTest : BitwardenComposeTest() {
|
||||
fun setUp() {
|
||||
setContent {
|
||||
WelcomeScreen(
|
||||
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
|
||||
onNavigateToLogin = { onNavigateToLoginCalled = true },
|
||||
onNavigateToStartRegistration = { onNavigateToStartRegistrationCalled = true },
|
||||
viewModel = viewModel,
|
||||
@ -85,12 +83,6 @@ class WelcomeScreenTest : BitwardenComposeTest() {
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCreateAccount event should call onNavigateToCreateAccount`() {
|
||||
mutableEventFlow.tryEmit(WelcomeEvent.NavigateToCreateAccount)
|
||||
assertTrue(onNavigateToCreateAccountCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
||||
mutableEventFlow.tryEmit(WelcomeEvent.NavigateToLogin)
|
||||
|
||||
@ -2,21 +2,15 @@ package com.x8bit.bitwarden.ui.auth.feature.welcome
|
||||
|
||||
import app.cash.turbine.test
|
||||
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 org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class WelcomeViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>()
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
@ -28,7 +22,7 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `PagerSwipe should update state`() = runTest {
|
||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
||||
val viewModel = createViewModel()
|
||||
val newIndex = 2
|
||||
|
||||
viewModel.trySendAction(WelcomeAction.PagerSwipe(index = newIndex))
|
||||
@ -43,7 +37,7 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `DotClick should update state and emit UpdatePager`() = runTest {
|
||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
||||
val viewModel = createViewModel()
|
||||
val newIndex = 2
|
||||
|
||||
viewModel.trySendAction(WelcomeAction.DotClick(index = newIndex))
|
||||
@ -62,41 +56,22 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `CreateAccountClick should emit NavigateToCreateAccount when email verification is disabled`() =
|
||||
runTest {
|
||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.EmailVerification) } returns false
|
||||
viewModel.trySendAction(WelcomeAction.CreateAccountClick)
|
||||
fun `CreateAccountClick should emit NavigateToStartRegistration`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(WelcomeAction.CreateAccountClick)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
WelcomeEvent.NavigateToCreateAccount,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
WelcomeEvent.NavigateToStartRegistration,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginClick should emit NavigateToLogin`() = runTest {
|
||||
val viewModel = WelcomeViewModel(featureFlagManager = featureFlagManager)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(WelcomeAction.LoginClick)
|
||||
|
||||
@ -107,6 +82,8 @@ class WelcomeViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): WelcomeViewModel = WelcomeViewModel()
|
||||
}
|
||||
|
||||
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.performClick
|
||||
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.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
@ -85,13 +85,13 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
mutableStateFlow.tryEmit(
|
||||
DebugMenuState(
|
||||
featureFlags = persistentMapOf(
|
||||
FlagKey.EmailVerification to true,
|
||||
FlagKey.CredentialExchangeProtocolImport to true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Email Verification", ignoreCase = true)
|
||||
.onNodeWithText("CXP Import", ignoreCase = true)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@ -100,18 +100,18 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
mutableStateFlow.tryEmit(
|
||||
DebugMenuState(
|
||||
featureFlags = persistentMapOf(
|
||||
FlagKey.EmailVerification to true,
|
||||
FlagKey.CredentialExchangeProtocolImport to true,
|
||||
),
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNodeWithText("Email Verification", ignoreCase = true)
|
||||
.onNodeWithText("CXP Import", ignoreCase = true)
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.UpdateFeatureFlag(
|
||||
FlagKey.EmailVerification,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
false,
|
||||
),
|
||||
)
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.debugmenu
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
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.util.assertCoroutineThrows
|
||||
import io.mockk.coEvery
|
||||
@ -101,10 +101,10 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||
fun `handleUpdateFeatureFlag should update the feature flag via the repository`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.UpdateFeatureFlag(FlagKey.EmailVerification, false),
|
||||
DebugMenuAction.UpdateFeatureFlag(FlagKey.CipherKeyEncryption, false),
|
||||
)
|
||||
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(
|
||||
FlagKey.EmailVerification to true,
|
||||
FlagKey.CredentialExchangeProtocolImport to true,
|
||||
FlagKey.CredentialExchangeProtocolExport 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(
|
||||
FlagKey.EmailVerification to false,
|
||||
FlagKey.CredentialExchangeProtocolImport to false,
|
||||
FlagKey.CredentialExchangeProtocolExport to false,
|
||||
FlagKey.RestrictCipherItemDeletion to false,
|
||||
|
||||
@ -26,7 +26,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
FlagKey.EmailVerification,
|
||||
FlagKey.RemoveCardPolicy,
|
||||
FlagKey.RestrictCipherItemDeletion,
|
||||
FlagKey.UserManagedPrivilegedApps,
|
||||
@ -67,7 +66,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
FlagKey.DummyString,
|
||||
-> this.keyName
|
||||
|
||||
FlagKey.EmailVerification -> stringResource(BitwardenString.email_verification)
|
||||
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
||||
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
||||
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
||||
|
||||
@ -30,7 +30,6 @@ sealed class FlagKey<out T : Any> {
|
||||
*/
|
||||
val activePasswordManagerFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
EmailVerification,
|
||||
CredentialExchangeProtocolImport,
|
||||
CredentialExchangeProtocolExport,
|
||||
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
|
||||
* feature.
|
||||
|
||||
@ -8,10 +8,6 @@ class FlagKeyTest {
|
||||
|
||||
@Test
|
||||
fun `Feature flags have the correct key name set`() {
|
||||
assertEquals(
|
||||
FlagKey.EmailVerification.keyName,
|
||||
"email-verification",
|
||||
)
|
||||
assertEquals(
|
||||
FlagKey.CredentialExchangeProtocolImport.keyName,
|
||||
"cxp-import-mobile",
|
||||
@ -46,7 +42,6 @@ class FlagKeyTest {
|
||||
fun `All feature flags have the correct default value set`() {
|
||||
assertTrue(
|
||||
listOf(
|
||||
FlagKey.EmailVerification,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
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_message">Password does not meet organization requirements. Please check the policy information and try again.</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="privacy_policy">Privacy Policy</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_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="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_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>
|
||||
@ -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="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_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="check_your_email">Check your email</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>
|
||||
|
||||
<!-- region Debug Menu -->
|
||||
<string name="email_verification">Email Verification</string>
|
||||
<string name="feature_flags">Feature Flags:</string>
|
||||
<string name="debug_menu">Debug Menu</string>
|
||||
<string name="reset_values">Reset values</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user