mirror of
https://github.com/bitwarden/android.git
synced 2026-04-25 15:28:09 -05:00
PM-18123 Update the reset password screen. (#4719)
This commit is contained in:
@@ -362,8 +362,10 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
|
||||
/**
|
||||
* Get the password strength for the given [email] and [password] combo.
|
||||
* If no value is passed for the [email] will use the active email of the current active
|
||||
* account via the [userStateFlow].
|
||||
*/
|
||||
suspend fun getPasswordStrength(email: String, password: String): PasswordStrengthResult
|
||||
suspend fun getPasswordStrength(email: String? = null, password: String): PasswordStrengthResult
|
||||
|
||||
/**
|
||||
* Validates the master password for the current logged in user.
|
||||
|
||||
@@ -1196,12 +1196,17 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
|
||||
override suspend fun getPasswordStrength(
|
||||
email: String,
|
||||
email: String?,
|
||||
password: String,
|
||||
): PasswordStrengthResult =
|
||||
authSdkSource
|
||||
.passwordStrength(
|
||||
email = email,
|
||||
email = email
|
||||
?: userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.email
|
||||
.orEmpty(),
|
||||
password = password,
|
||||
)
|
||||
.fold(
|
||||
|
||||
@@ -105,7 +105,7 @@ private fun RemovePasswordScreenContent(
|
||||
value = state.input,
|
||||
onValueChange = onInputChanged,
|
||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
||||
textFieldTestTag = "MasterPasswordEntry",
|
||||
passwordFieldTestTag = "MasterPasswordEntry",
|
||||
autoFocus = true,
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -10,17 +10,21 @@ const val RESET_PASSWORD_ROUTE: String = "reset_password"
|
||||
/**
|
||||
* Add the Reset Password screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.resetPasswordDestination() {
|
||||
fun NavGraphBuilder.resetPasswordDestination(
|
||||
onNavigateToPreventAccountLockOut: () -> Unit,
|
||||
) {
|
||||
composable(
|
||||
route = RESET_PASSWORD_ROUTE,
|
||||
) {
|
||||
ResetPasswordScreen()
|
||||
ResetPasswordScreen(onNavigateToPreventAccountLockOut = onNavigateToPreventAccountLockOut)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the Reset Password screen.
|
||||
*/
|
||||
fun NavController.navigateToResetPasswordGraph(navOptions: NavOptions? = null) {
|
||||
fun NavController.navigateToResetPasswordScreen(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate(RESET_PASSWORD_ROUTE, navOptions)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.resetpassword
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -10,6 +11,7 @@ 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
|
||||
@@ -23,13 +25,19 @@ 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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
@@ -39,6 +47,9 @@ 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.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* The top level composable for the Reset Password screen.
|
||||
@@ -47,10 +58,19 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun ResetPasswordScreen(
|
||||
onNavigateToPreventAccountLockOut: () -> Unit,
|
||||
viewModel: ResetPasswordViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
ResetPasswordEvent.NavigateToPreventAccountLockout -> {
|
||||
onNavigateToPreventAccountLockOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val dialog = state.dialogState) {
|
||||
is ResetPasswordState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
@@ -69,7 +89,7 @@ fun ResetPasswordScreen(
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
var shouldShowLogoutConfirmationDialog by remember { mutableStateOf(false) }
|
||||
var shouldShowLogoutConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val onLogoutClicked = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ResetPasswordAction.ConfirmLogoutClick) }
|
||||
}
|
||||
@@ -100,16 +120,19 @@ fun ResetPasswordScreen(
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.log_out),
|
||||
onClick = { shouldShowLogoutConfirmationDialog = true },
|
||||
modifier = Modifier.testTag("LogoutButton"),
|
||||
)
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.submit),
|
||||
label = stringResource(id = R.string.save),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ResetPasswordAction.SubmitClick) }
|
||||
{ viewModel.trySendAction(ResetPasswordAction.SaveClick) }
|
||||
},
|
||||
modifier = Modifier.testTag("SubmitButton"),
|
||||
modifier = Modifier.testTag("SaveButton"),
|
||||
)
|
||||
BitwardenOverflowActionItem(
|
||||
menuItemDataList = persistentListOf(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(R.string.log_out),
|
||||
onClick = { shouldShowLogoutConfirmationDialog = true },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -129,6 +152,9 @@ fun ResetPasswordScreen(
|
||||
onPasswordHintInputChanged = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged(it)) }
|
||||
},
|
||||
onLearnToPreventLockout = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick) }
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -142,6 +168,7 @@ private fun ResetPasswordScreenContent(
|
||||
onPasswordInputChanged: (String) -> Unit,
|
||||
onRetypePasswordInputChanged: (String) -> Unit,
|
||||
onPasswordHintInputChanged: (String) -> Unit,
|
||||
onLearnToPreventLockout: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -180,12 +207,11 @@ private fun ResetPasswordScreenContent(
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.current_master_password),
|
||||
label = stringResource(id = R.string.current_master_password_required),
|
||||
value = state.currentPasswordInput,
|
||||
onValueChange = onCurrentPasswordInputChanged,
|
||||
textFieldTestTag = "MasterPasswordField",
|
||||
passwordFieldTestTag = "MasterPasswordField",
|
||||
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -195,12 +221,20 @@ private fun ResetPasswordScreenContent(
|
||||
|
||||
var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
label = stringResource(id = R.string.new_master_password_required),
|
||||
value = state.passwordInput,
|
||||
onValueChange = onPasswordInputChanged,
|
||||
showPassword = isPasswordVisible,
|
||||
showPasswordChange = { isPasswordVisible = it },
|
||||
passwordFieldTestTag = "NewPasswordField",
|
||||
supportingContent = {
|
||||
PasswordStrengthIndicator(
|
||||
state = state.passwordStrengthState,
|
||||
currentCharacterCount = state.passwordInput.length,
|
||||
minimumCharacterCount = state.minimumPasswordLength,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
cardStyle = if (
|
||||
state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) {
|
||||
CardStyle.Middle(dividerPadding = 0.dp)
|
||||
@@ -213,7 +247,7 @@ private fun ResetPasswordScreenContent(
|
||||
)
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.retype_master_password),
|
||||
label = stringResource(id = R.string.retype_new_master_password_required),
|
||||
value = state.retypePasswordInput,
|
||||
onValueChange = onRetypePasswordInputChanged,
|
||||
showPassword = isPasswordVisible,
|
||||
@@ -226,10 +260,29 @@ private fun ResetPasswordScreenContent(
|
||||
)
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.master_password_hint),
|
||||
label = stringResource(id = R.string.new_master_password_hint),
|
||||
value = state.passwordHintInput,
|
||||
onValueChange = onPasswordHintInputChanged,
|
||||
supportingText = stringResource(id = R.string.master_password_hint_description),
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.bitwarden_cannot_reset_a_lost_or_forgotten_master_password,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
BitwardenHyperTextLink(
|
||||
annotatedResId = R.string.learn_about_ways_to_prevent_account_lockout,
|
||||
annotationKey = "onPreventAccountLockout",
|
||||
accessibilityString = stringResource(
|
||||
R.string.learn_about_ways_to_prevent_account_lockout,
|
||||
),
|
||||
onClick = onLearnToPreventLockout,
|
||||
)
|
||||
}
|
||||
},
|
||||
textFieldTestTag = "MasterPasswordHintLabel",
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier
|
||||
@@ -240,3 +293,31 @@ private fun ResetPasswordScreenContent(
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ResetPasswordScreenContent_preview() {
|
||||
BitwardenTheme {
|
||||
ResetPasswordScreenContent(
|
||||
state = ResetPasswordState(
|
||||
policies = listOf(),
|
||||
resetReason = null,
|
||||
dialogState = null,
|
||||
currentPasswordInput = "",
|
||||
passwordInput = "",
|
||||
retypePasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
passwordStrengthState = PasswordStrengthState.GOOD,
|
||||
minimumPasswordLength = 12,
|
||||
),
|
||||
onCurrentPasswordInputChanged = {},
|
||||
onPasswordInputChanged = {},
|
||||
onRetypePasswordInputChanged = {},
|
||||
onPasswordHintInputChanged = {},
|
||||
onLearnToPreventLockout = {},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(BitwardenTheme.colorScheme.background.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,20 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
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.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.util.toDisplayLabels
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.util.toStrictestPolicy
|
||||
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
|
||||
@@ -34,16 +39,30 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<ResetPasswordState, ResetPasswordEvent, ResetPasswordAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: ResetPasswordState(
|
||||
policies = authRepository.passwordPolicies.toDisplayLabels(),
|
||||
resetReason = authRepository.passwordResetReason,
|
||||
dialogState = null,
|
||||
currentPasswordInput = "",
|
||||
passwordInput = "",
|
||||
retypePasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
),
|
||||
?: run {
|
||||
val policies = authRepository.passwordPolicies
|
||||
ResetPasswordState(
|
||||
policies = policies.toDisplayLabels(),
|
||||
resetReason = authRepository.passwordResetReason,
|
||||
dialogState = null,
|
||||
currentPasswordInput = "",
|
||||
passwordInput = "",
|
||||
retypePasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
minimumPasswordLength = policies
|
||||
.toStrictestPolicy()
|
||||
.minLength
|
||||
?: MIN_PASSWORD_LENGTH,
|
||||
)
|
||||
},
|
||||
) {
|
||||
/**
|
||||
* 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
|
||||
@@ -54,7 +73,7 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
override fun handleAction(action: ResetPasswordAction) {
|
||||
when (action) {
|
||||
ResetPasswordAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
|
||||
ResetPasswordAction.SubmitClick -> handleSubmitClicked()
|
||||
ResetPasswordAction.SaveClick -> handleSaveClicked()
|
||||
ResetPasswordAction.DialogDismiss -> handleDialogDismiss()
|
||||
|
||||
is ResetPasswordAction.CurrentPasswordInputChanged -> {
|
||||
@@ -82,6 +101,58 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
is ResetPasswordAction.Internal.ReceiveValidatePasswordResult -> {
|
||||
handleReceiveValidatePasswordResult(action)
|
||||
}
|
||||
|
||||
is ResetPasswordAction.Internal.ReceivePasswordStrengthResult -> {
|
||||
handlePasswordStrengthResult(action)
|
||||
}
|
||||
|
||||
ResetPasswordAction.LearnHowPreventLockoutClick -> {
|
||||
handleLearnHowToPreventLockoutClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLearnHowToPreventLockoutClick() {
|
||||
sendEvent(ResetPasswordEvent.NavigateToPreventAccountLockout)
|
||||
}
|
||||
|
||||
private fun checkPasswordStrength(input: String) {
|
||||
// Update password strength:
|
||||
passwordStrengthJob.cancel()
|
||||
if (input.isEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
|
||||
}
|
||||
} else {
|
||||
passwordStrengthJob = viewModelScope.launch {
|
||||
val result = authRepository.getPasswordStrength(
|
||||
password = input,
|
||||
)
|
||||
trySendAction(ResetPasswordAction.Internal.ReceivePasswordStrengthResult(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePasswordStrengthResult(
|
||||
action: ResetPasswordAction.Internal.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 {
|
||||
it.copy(
|
||||
passwordStrengthState = updatedState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PasswordStrengthResult.Error -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +166,7 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
/**
|
||||
* Validate the user's current password when they submit.
|
||||
*/
|
||||
private fun handleSubmitClicked() {
|
||||
private fun handleSaveClicked() {
|
||||
// Display an error dialog if the new password field is blank.
|
||||
if (state.passwordInput.isBlank()) {
|
||||
mutableStateFlow.update {
|
||||
@@ -175,6 +246,7 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
passwordInput = action.input,
|
||||
)
|
||||
}
|
||||
checkPasswordStrength(input = action.input)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,6 +426,8 @@ data class ResetPasswordState(
|
||||
val passwordInput: String,
|
||||
val retypePasswordInput: String,
|
||||
val passwordHintInput: String,
|
||||
val passwordStrengthState: PasswordStrengthState,
|
||||
val minimumPasswordLength: Int,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
@@ -382,7 +456,13 @@ data class ResetPasswordState(
|
||||
/**
|
||||
* Models events for the Reset Password screen.
|
||||
*/
|
||||
sealed class ResetPasswordEvent
|
||||
sealed class ResetPasswordEvent {
|
||||
|
||||
/**
|
||||
* Navigate to the LearnToPreventLockout screen
|
||||
*/
|
||||
data object NavigateToPreventAccountLockout : ResetPasswordEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the Reset Password screen.
|
||||
@@ -394,9 +474,9 @@ sealed class ResetPasswordAction {
|
||||
data object ConfirmLogoutClick : ResetPasswordAction()
|
||||
|
||||
/**
|
||||
* Indicates that the user has clicked the submit button.
|
||||
* Indicates that the user has clicked the save button.
|
||||
*/
|
||||
data object SubmitClick : ResetPasswordAction()
|
||||
data object SaveClick : ResetPasswordAction()
|
||||
|
||||
/**
|
||||
* Indicates that the dialog has been dismissed.
|
||||
@@ -423,6 +503,11 @@ sealed class ResetPasswordAction {
|
||||
*/
|
||||
data class PasswordHintInputChanged(val input: String) : ResetPasswordAction()
|
||||
|
||||
/**
|
||||
* Indicates user clicked the "Learn ways..." text
|
||||
*/
|
||||
data object LearnHowPreventLockoutClick : ResetPasswordAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [ResetPasswordViewModel] might send itself.
|
||||
*/
|
||||
@@ -447,5 +532,12 @@ sealed class ResetPasswordAction {
|
||||
data class ReceiveValidatePasswordAgainstPoliciesResult(
|
||||
val meetsRequirements: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a password strength result has been received.
|
||||
*/
|
||||
data class ReceivePasswordStrengthResult(
|
||||
val result: PasswordStrengthResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ fun VaultUnlockScreen(
|
||||
},
|
||||
),
|
||||
supportingText = state.vaultUnlockType.unlockScreenMessage(),
|
||||
textFieldTestTag = state.vaultUnlockType.unlockScreenInputTestTag,
|
||||
passwordFieldTestTag = state.vaultUnlockType.unlockScreenInputTestTag,
|
||||
cardStyle = CardStyle.Top(hasDivider = false),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
@@ -45,7 +45,7 @@ fun BitwardenOverflowActionItem(
|
||||
menuItemDataList: ImmutableList<OverflowMenuItemData> = persistentListOf(),
|
||||
) {
|
||||
if (menuItemDataList.isEmpty()) return
|
||||
var isOverflowMenuVisible by remember { mutableStateOf(false) }
|
||||
var isOverflowMenuVisible by rememberSaveable { mutableStateOf(false) }
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier,
|
||||
|
||||
@@ -94,7 +94,7 @@ fun BitwardenClientCertificateDialog(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
cardStyle = CardStyle.Bottom,
|
||||
textFieldTestTag = "AlertClientCertificatePasswordInputField",
|
||||
passwordFieldTestTag = "AlertClientCertificatePasswordInputField",
|
||||
modifier = Modifier.imePadding(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ fun BitwardenMasterPasswordDialog(
|
||||
value = masterPassword,
|
||||
onValueChange = { masterPassword = it },
|
||||
autoFocus = true,
|
||||
textFieldTestTag = "AlertInputField",
|
||||
passwordFieldTestTag = "AlertInputField",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier.imePadding(),
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ fun BitwardenPinDialog(
|
||||
value = pin,
|
||||
onValueChange = { pin = it },
|
||||
autoFocus = true,
|
||||
textFieldTestTag = "AlertInputField",
|
||||
passwordFieldTestTag = "AlertInputField",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -398,7 +398,7 @@ fun BitwardenPasswordField(
|
||||
* the password field.
|
||||
* @param imeAction the preferred IME action for the keyboard to have.
|
||||
* @param keyboardActions the callbacks of keyboard actions.
|
||||
* @param textFieldTestTag The optional test tag associated with the inner text field.
|
||||
* @param passwordFieldTestTag The optional test tag associated with the inner text field.
|
||||
* @param cardStyle Indicates the type of card style to be applied.
|
||||
* @param actionsPadding Padding to be applied to the [actions] block.
|
||||
* @param actions A lambda containing the set of actions (usually icons or similar) to display
|
||||
@@ -421,7 +421,7 @@ fun BitwardenPasswordField(
|
||||
keyboardType: KeyboardType = KeyboardType.Password,
|
||||
imeAction: ImeAction = ImeAction.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
textFieldTestTag: String? = null,
|
||||
passwordFieldTestTag: String? = null,
|
||||
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
|
||||
actions: @Composable (RowScope.() -> Unit)? = null,
|
||||
) {
|
||||
@@ -441,7 +441,7 @@ fun BitwardenPasswordField(
|
||||
keyboardType = keyboardType,
|
||||
imeAction = imeAction,
|
||||
keyboardActions = keyboardActions,
|
||||
passwordFieldTestTag = textFieldTestTag,
|
||||
passwordFieldTestTag = passwordFieldTestTag,
|
||||
cardStyle = cardStyle,
|
||||
actionsPadding = actionsPadding,
|
||||
actions = actions,
|
||||
|
||||
@@ -29,11 +29,12 @@ import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
|
||||
import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.navigateToExpiredRegistrationLinkScreen
|
||||
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.navigateToNewDeviceNoticeEmailAccess
|
||||
import com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout.navigateToPreventAccountLockout
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.REMOVE_PASSWORD_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.navigateToRemovePassword
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.removePasswordDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.RESET_PASSWORD_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordScreen
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.setpassword.SET_PASSWORD_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
|
||||
@@ -94,7 +95,11 @@ fun RootNavScreen(
|
||||
splashDestination()
|
||||
authGraph(navController)
|
||||
removePasswordDestination()
|
||||
resetPasswordDestination()
|
||||
resetPasswordDestination(
|
||||
onNavigateToPreventAccountLockOut = {
|
||||
navController.navigateToPreventAccountLockout()
|
||||
},
|
||||
)
|
||||
trustedDeviceGraph(navController)
|
||||
vaultUnlockDestination()
|
||||
vaultUnlockedGraph(navController)
|
||||
@@ -182,7 +187,9 @@ fun RootNavScreen(
|
||||
}
|
||||
|
||||
RootNavState.RemovePassword -> navController.navigateToRemovePassword(rootNavOptions)
|
||||
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
|
||||
RootNavState.ResetPassword -> {
|
||||
navController.navigateToResetPasswordScreen(rootNavOptions)
|
||||
}
|
||||
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
|
||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||
RootNavState.TrustedDevice -> navController.navigateToTrustedDeviceGraph(rootNavOptions)
|
||||
|
||||
@@ -321,7 +321,7 @@ private fun ExportVaultScreenContent(
|
||||
value = state.passwordInput,
|
||||
readOnly = state.policyPreventsExport,
|
||||
onValueChange = onPasswordInputChanged,
|
||||
textFieldTestTag = "MasterPasswordEntry",
|
||||
passwordFieldTestTag = "MasterPasswordEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
|
||||
@@ -54,7 +54,6 @@ class ExportVaultViewModel @Inject constructor(
|
||||
?: ExportVaultState(
|
||||
confirmFilePasswordInput = "",
|
||||
dialogState = null,
|
||||
email = requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email),
|
||||
exportData = null,
|
||||
exportFormat = ExportVaultFormat.JSON,
|
||||
filePasswordInput = "",
|
||||
@@ -266,7 +265,6 @@ class ExportVaultViewModel @Inject constructor(
|
||||
} else {
|
||||
passwordStrengthJob = viewModelScope.launch {
|
||||
val result = authRepository.getPasswordStrength(
|
||||
email = state.email,
|
||||
password = action.input,
|
||||
)
|
||||
trySendAction(ExportVaultAction.Internal.ReceivePasswordStrengthResult(result))
|
||||
@@ -458,7 +456,6 @@ data class ExportVaultState(
|
||||
val exportData: String? = null,
|
||||
val confirmFilePasswordInput: String,
|
||||
val dialogState: DialogState?,
|
||||
val email: String,
|
||||
val exportFormat: ExportVaultFormat,
|
||||
val filePasswordInput: String,
|
||||
val passwordInput: String,
|
||||
|
||||
@@ -1207,7 +1207,7 @@ private fun ForwardedEmailAliasTypeContent(
|
||||
value = usernameTypeState.selectedServiceType.apiAccessToken,
|
||||
onValueChange = forwardedEmailAliasHandlers.onAddyIoAccessTokenTextChange,
|
||||
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
|
||||
textFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -1234,7 +1234,7 @@ private fun ForwardedEmailAliasTypeContent(
|
||||
value = usernameTypeState.selectedServiceType.apiKey,
|
||||
onValueChange = forwardedEmailAliasHandlers.onDuckDuckGoApiKeyTextChange,
|
||||
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
|
||||
textFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -1248,7 +1248,7 @@ private fun ForwardedEmailAliasTypeContent(
|
||||
value = usernameTypeState.selectedServiceType.apiKey,
|
||||
onValueChange = forwardedEmailAliasHandlers.onFastMailApiKeyTextChange,
|
||||
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
|
||||
textFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -1262,7 +1262,7 @@ private fun ForwardedEmailAliasTypeContent(
|
||||
value = usernameTypeState.selectedServiceType.apiAccessToken,
|
||||
onValueChange = forwardedEmailAliasHandlers.onFirefoxRelayAccessTokenTextChange,
|
||||
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
|
||||
textFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -1276,7 +1276,7 @@ private fun ForwardedEmailAliasTypeContent(
|
||||
value = usernameTypeState.selectedServiceType.apiKey,
|
||||
onValueChange = forwardedEmailAliasHandlers.onForwardEmailApiKeyTextChange,
|
||||
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
|
||||
textFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -1303,7 +1303,7 @@ private fun ForwardedEmailAliasTypeContent(
|
||||
value = usernameTypeState.selectedServiceType.apiKey,
|
||||
onValueChange = forwardedEmailAliasHandlers.onSimpleLoginApiKeyTextChange,
|
||||
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
|
||||
textFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -1318,7 +1318,7 @@ private fun ForwardedEmailAliasTypeContent(
|
||||
value = obfuscatedTextField,
|
||||
onValueChange = { obfuscatedTextField = it },
|
||||
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
|
||||
textFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
|
||||
@@ -377,7 +377,7 @@ private fun AddSendOptions(
|
||||
readOnly = sendRestrictionPolicy,
|
||||
value = state.common.passwordInput,
|
||||
onValueChange = addSendHandlers.onPasswordChange,
|
||||
textFieldTestTag = "SendNewPasswordEntry",
|
||||
passwordFieldTestTag = "SendNewPasswordEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<string name="switch_to_already_added_account_confirmation">Would you like to switch to it now?</string>
|
||||
<string name="master_password">Master password</string>
|
||||
<string name="master_password_required">Master password (required)</string>
|
||||
<string name="new_master_password_required">New master password (required)</string>
|
||||
<string name="more">More</string>
|
||||
<string name="my_vault">My vault</string>
|
||||
<string name="authenticator">Authenticator</string>
|
||||
@@ -186,6 +187,7 @@
|
||||
<string name="rate_the_app_description">Please consider helping us out with a good review!</string>
|
||||
<string name="regenerate_password">Regenerate password</string>
|
||||
<string name="retype_master_password">Re-type master password</string>
|
||||
<string name="retype_new_master_password_required">Re-type new master password (required)</string>
|
||||
<string name="retype_master_password_required">Re-type master password (required)</string>
|
||||
<string name="search_vault">Search vault</string>
|
||||
<string name="security">Security</string>
|
||||
@@ -829,7 +831,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="data_region">Data region</string>
|
||||
<string name="region">Region</string>
|
||||
<string name="update_weak_master_password_warning">Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</string>
|
||||
<string name="current_master_password">Current master password</string>
|
||||
<string name="current_master_password_required">Current master password (required)</string>
|
||||
<string name="logged_in">Logged in!</string>
|
||||
<string name="approve_with_my_other_device">Approve with my other device</string>
|
||||
<string name="request_admin_approval">Request admin approval</string>
|
||||
@@ -1021,12 +1023,14 @@ Do you want to switch to this account?</string>
|
||||
<string name="generate_button_label">Generate</string>
|
||||
<string name="write_this_password_down_and_keep_it_somewhere_safe">Write this password down and keep it somewhere safe.</string>
|
||||
<string name="learn_about_other_ways_to_prevent_account_lockout">Learn about other ways to prevent account lockout</string>
|
||||
<string name="learn_about_ways_to_prevent_account_lockout"><annotation link="onPreventAccountLockout">Learn about ways to prevent account lockout</annotation></string>
|
||||
<string name="help_with_server_geolocations">Help with server geolocations.</string>
|
||||
<string name="email_address_required">Email address (required)</string>
|
||||
<string name="select_the_link_in_the_email_to_verify_your_email_address_and_continue_creating_your_account">"Select the link in the email to verify your email address and continue creating your account. "</string>
|
||||
<string name="change_email_address">Change email address</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="bitwarden_cannot_recover_a_lost_or_forgotten_master_password">Bitwarden cannot recover a lost or forgotten master password.</string>
|
||||
<string name="bitwarden_cannot_reset_a_lost_or_forgotten_master_password">Bitwarden cannot reset a lost or forgotten master password.</string>
|
||||
<string name="choose_your_master_password">Choose your master password</string>
|
||||
<string name="choose_a_unique_and_strong_password_to_keep_your_information_safe">Choose a unique and strong password to keep your information safe.</string>
|
||||
<string name="minimum_characters">%1$s characters</string>
|
||||
@@ -1046,6 +1050,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="error_connecting_with_the_duo_service_use_a_different_two_step_login_method_or_contact_duo_for_assistance">Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance.</string>
|
||||
<string name="master_password_hint_not_specified">Master password hint</string>
|
||||
<string name="master_password_important_hint">Important: Your master password cannot be recovered if you forget it! 12 characters minimum.</string>
|
||||
<string name="new_master_password_hint">New master password hint</string>
|
||||
<string name="get_started">Get started</string>
|
||||
<string name="save_and_protect_your_data">Save and protect your data</string>
|
||||
<string name="the_vault_protects_more_than_just_passwords">The vault protects more than just passwords. Store secure logins, IDs, cards and notes securely here.</string>
|
||||
|
||||
@@ -7,11 +7,13 @@ import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordAction
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordEvent
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordScreen
|
||||
@@ -20,15 +22,18 @@ import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.x8bit.bitwarden.ui.util.performCustomAccessibilityAction
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
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 ResetPasswordScreenTest : BaseComposeTest() {
|
||||
private var onNavigateToLearnToPreventLockoutCalled = false
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<ResetPasswordEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<ResetPasswordViewModel>(relaxed = true) {
|
||||
@@ -40,6 +45,9 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
ResetPasswordScreen(
|
||||
onNavigateToPreventAccountLockOut = {
|
||||
onNavigateToLearnToPreventLockoutCalled = true
|
||||
},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -82,9 +90,16 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
composeTestRule.onNodeWithText("Loading...").isDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `logout button click should display confirmation dialog and emit ConfirmLogoutClick`() {
|
||||
composeTestRule.onNodeWithText("Log out").performClick()
|
||||
fun `logout button click from more menu should display confirmation dialog and emit ConfirmLogoutClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("More")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Log out")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Are you sure you want to log out?")
|
||||
@@ -104,10 +119,12 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `submit button click should emit SubmitClick`() {
|
||||
composeTestRule.onNodeWithText("Submit").performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Save")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +193,9 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
@Test
|
||||
fun `current password input change should send CurrentPasswordInputChanged action`() {
|
||||
val input = "Test123"
|
||||
composeTestRule.onNodeWithText("Current master password").performTextInput(input)
|
||||
composeTestRule
|
||||
.onNodeWithText("Current master password (required)")
|
||||
.performTextInput(input)
|
||||
verify {
|
||||
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged("Test123"))
|
||||
}
|
||||
@@ -185,7 +204,7 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
@Test
|
||||
fun `current password field should update according to state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Current master password")
|
||||
.onNodeWithText("Current master password (required)")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
@@ -195,14 +214,16 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Current master password")
|
||||
.onNodeWithText("Current master password (required)")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `password input change should send PasswordInputChange action`() {
|
||||
val input = "Test123"
|
||||
composeTestRule.onNodeWithText("Master password").performTextInput(input)
|
||||
composeTestRule
|
||||
.onNodeWithText("New master password (required)")
|
||||
.performTextInput(input)
|
||||
verify {
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
|
||||
}
|
||||
@@ -211,7 +232,9 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
@Test
|
||||
fun `retype password input change should send RetypePasswordInputChanged action`() {
|
||||
val input = "Test123"
|
||||
composeTestRule.onNodeWithText("Re-type master password").performTextInput(input)
|
||||
composeTestRule
|
||||
.onNodeWithText("Re-type new master password (required)")
|
||||
.performTextInput(input)
|
||||
verify {
|
||||
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged("Test123"))
|
||||
}
|
||||
@@ -220,7 +243,7 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
@Test
|
||||
fun `password hint input change should send PasswordHintInputChanged action`() {
|
||||
val input = "Test123"
|
||||
composeTestRule.onNodeWithText("Master password hint (optional)").performTextInput(input)
|
||||
composeTestRule.onNodeWithText("New master password hint").performTextInput(input)
|
||||
verify {
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged("Test123"))
|
||||
}
|
||||
@@ -250,6 +273,21 @@ class ResetPasswordScreenTest : BaseComposeTest() {
|
||||
.onAllNodesWithContentDescription("Show")
|
||||
.assertCountEquals(3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToPreventAccountLockout event calls onNavigateToPreventAccountLockout`() {
|
||||
mutableEventFlow.tryEmit(ResetPasswordEvent.NavigateToPreventAccountLockout)
|
||||
assertTrue(onNavigateToLearnToPreventLockoutCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `When learn new ways text is clicked, send correct action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about ways to prevent account lockout")
|
||||
.performCustomAccessibilityAction("Learn about ways to prevent account lockout")
|
||||
|
||||
verify { viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick) }
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = ResetPasswordState(
|
||||
@@ -260,4 +298,6 @@ private val DEFAULT_STATE = ResetPasswordState(
|
||||
passwordInput = "",
|
||||
retypePasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
minimumPasswordLength = 12,
|
||||
)
|
||||
|
||||
@@ -4,10 +4,14 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
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.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordAction
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordEvent
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordState
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
@@ -57,7 +61,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `SubmitClicked with blank password shows error alert`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -84,10 +88,13 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
coEvery {
|
||||
authRepository.validatePasswordAgainstPolicies(password)
|
||||
} returns false
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(password = any())
|
||||
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -96,6 +103,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
message = R.string.master_password_policy_validation_message.asText(),
|
||||
),
|
||||
passwordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@@ -107,10 +115,13 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
every {
|
||||
authRepository.passwordResetReason
|
||||
} returns ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(password = any())
|
||||
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -121,6 +132,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
passwordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@@ -132,11 +144,14 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
coEvery {
|
||||
authRepository.validatePasswordAgainstPolicies(password)
|
||||
} returns true
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(password = any())
|
||||
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -145,6 +160,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
),
|
||||
passwordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@@ -160,13 +176,16 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
coEvery {
|
||||
authRepository.validatePassword(currentPassword)
|
||||
} returns ValidatePasswordResult.Error
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(password = any())
|
||||
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
|
||||
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -177,6 +196,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
currentPasswordInput = currentPassword,
|
||||
passwordInput = password,
|
||||
retypePasswordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@@ -192,13 +212,16 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
coEvery {
|
||||
authRepository.validatePassword(currentPassword)
|
||||
} returns ValidatePasswordResult.Success(isValid = false)
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(password = any())
|
||||
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
|
||||
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -209,6 +232,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
currentPasswordInput = currentPassword,
|
||||
passwordInput = password,
|
||||
retypePasswordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@@ -227,6 +251,9 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
coEvery {
|
||||
authRepository.resetPassword(any(), any(), any())
|
||||
} returns ResetPasswordResult.Success
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(password = any())
|
||||
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
|
||||
@@ -240,11 +267,12 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
currentPasswordInput = currentPassword,
|
||||
passwordInput = password,
|
||||
retypePasswordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||
viewModel.trySendAction(ResetPasswordAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -254,6 +282,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
currentPasswordInput = currentPassword,
|
||||
passwordInput = password,
|
||||
retypePasswordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
@@ -264,6 +293,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
currentPasswordInput = currentPassword,
|
||||
passwordInput = password,
|
||||
retypePasswordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
@@ -272,18 +302,40 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PasswordInputChanged should update the password input in the state`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
|
||||
fun `PasswordInputChanged should update the password input in the state along with the password strength state`() =
|
||||
runTest {
|
||||
val passwordInput = "Test123"
|
||||
val viewModel = createViewModel()
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(password = any())
|
||||
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_4)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = "Test123",
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(input = passwordInput))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = passwordInput,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = passwordInput,
|
||||
passwordStrengthState = PasswordStrengthState.STRONG,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.getPasswordStrength(
|
||||
password = passwordInput,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RetypePasswordInputChanged should update the retype password input in the state`() {
|
||||
@@ -311,6 +363,19 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LearnHowPreventLockoutClick action sends NavigateToPreventAccountLockout event`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick)
|
||||
assertEquals(
|
||||
ResetPasswordEvent.NavigateToPreventAccountLockout,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): ResetPasswordViewModel =
|
||||
ResetPasswordViewModel(
|
||||
authRepository = authRepository,
|
||||
@@ -327,4 +392,6 @@ private val DEFAULT_STATE = ResetPasswordState(
|
||||
passwordInput = "",
|
||||
retypePasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
minimumPasswordLength = 12,
|
||||
)
|
||||
|
||||
@@ -305,7 +305,6 @@ class ExportVaultScreenTest : BaseComposeTest() {
|
||||
private val DEFAULT_STATE = ExportVaultState(
|
||||
confirmFilePasswordInput = "",
|
||||
dialogState = null,
|
||||
email = "test@bitwarden.com",
|
||||
exportFormat = ExportVaultFormat.JSON,
|
||||
filePasswordInput = "",
|
||||
passwordInput = "",
|
||||
|
||||
@@ -184,7 +184,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(
|
||||
email = EMAIL_ADDRESS,
|
||||
password = password,
|
||||
)
|
||||
} returns PasswordStrengthResult.Success(
|
||||
@@ -280,7 +279,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||
val password = "password"
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(
|
||||
email = EMAIL_ADDRESS,
|
||||
password = password,
|
||||
)
|
||||
} returns PasswordStrengthResult.Success(
|
||||
@@ -334,7 +332,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(
|
||||
email = EMAIL_ADDRESS,
|
||||
password = password,
|
||||
)
|
||||
} returns PasswordStrengthResult.Success(
|
||||
@@ -443,7 +440,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||
val password = "Test123"
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(
|
||||
email = EMAIL_ADDRESS,
|
||||
password = password,
|
||||
)
|
||||
} returns PasswordStrengthResult.Success(
|
||||
@@ -461,7 +457,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
coVerify {
|
||||
authRepository.getPasswordStrength(
|
||||
email = EMAIL_ADDRESS,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
@@ -750,14 +745,13 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
private const val EMAIL_ADDRESS = "active@bitwarden.com"
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "Active User",
|
||||
email = EMAIL_ADDRESS,
|
||||
email = "email",
|
||||
avatarColorHex = "#aa00aa",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
@@ -779,7 +773,6 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
private val DEFAULT_STATE = ExportVaultState(
|
||||
confirmFilePasswordInput = "",
|
||||
dialogState = null,
|
||||
email = EMAIL_ADDRESS,
|
||||
exportFormat = ExportVaultFormat.JSON,
|
||||
filePasswordInput = "",
|
||||
passwordInput = "",
|
||||
|
||||
Reference in New Issue
Block a user